Feat (#8161) Implement logic to fetch report data part-2 (#8605)

* Feat (#8161) Implement logic to fetch report data part-2

* Fix _get_user_details should return dict not None Type

* Format average session

* Make user name clickable

* Add provision for showing percentage symbol in graph tooltip

* Add summary support for entities

* Fix format issue

* Fix unit tests

* Fix labeling issue

* Add PageViewsByEntities chart

* Add DailyActiveUsers chart

* Add description to charts

* Fix unit tests

* remove startTs Overriding

* Address review comment
This commit is contained in:
Sachin Chaurasiya 2022-11-10 14:26:04 +05:30 committed by GitHub
parent b4e5f6ec13
commit 43ea44a0f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 562 additions and 144 deletions

View File

@ -225,7 +225,7 @@ class WebAnalyticUserActivityReportDataProcessor(DataProcessor):
"totalSessionDuration": total_session_duration_seconds, "totalSessionDuration": total_session_duration_seconds,
} }
def _get_user_details(self, user_id: str) -> Optional[dict]: def _get_user_details(self, user_id: str) -> dict:
"""Get user details from user id """Get user details from user id
Returns: Returns:
@ -239,7 +239,7 @@ class WebAnalyticUserActivityReportDataProcessor(DataProcessor):
) )
if not user_entity: if not user_entity:
return None return {}
teams = user_entity.teams teams = user_entity.teams
return { return {

View File

@ -11,7 +11,6 @@
* limitations under the License. * limitations under the License.
*/ */
import { DataReportIndex } from '../generated/dataInsight/dataInsightChart';
import { DataInsightChartResult } from '../generated/dataInsight/dataInsightChartResult'; import { DataInsightChartResult } from '../generated/dataInsight/dataInsightChartResult';
import { ChartAggregateParam } from '../interface/data-insight.interface'; import { ChartAggregateParam } from '../interface/data-insight.interface';
import APIClient from './index'; import APIClient from './index';
@ -20,10 +19,7 @@ export const getAggregateChartData = async (params: ChartAggregateParam) => {
const response = await APIClient.get<DataInsightChartResult>( const response = await APIClient.get<DataInsightChartResult>(
'/dataInsight/aggregate', '/dataInsight/aggregate',
{ {
params: { params,
...params,
dataReportIndex: DataReportIndex.EntityReportDataIndex,
},
} }
); );

View File

@ -0,0 +1,112 @@
/*
* 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 { Card, Typography } from 'antd';
import { AxiosError } from 'axios';
import React, { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { getAggregateChartData } from '../../axiosAPIs/DataInsightAPI';
import {
BAR_CHART_MARGIN,
DATA_INSIGHT_GRAPH_COLORS,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import { DataInsightChartType } from '../../generated/dataInsight/dataInsightChartResult';
import { DailyActiveUsers } from '../../generated/dataInsight/type/dailyActiveUsers';
import { ChartFilter } from '../../interface/data-insight.interface';
import {
CustomTooltip,
getFormattedActiveUsersData,
} from '../../utils/DataInsightUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import './DataInsightDetail.less';
interface Props {
chartFilter: ChartFilter;
}
const DailyActiveUsersChart: FC<Props> = ({ chartFilter }) => {
const [dailyActiveUsers, setDailyActiveUsers] = useState<DailyActiveUsers[]>(
[]
);
const [isLoading, setIsLoading] = useState<boolean>(false);
const { t } = useTranslation();
const fetchPageViewsByEntities = async () => {
setIsLoading(true);
try {
const params = {
...chartFilter,
dataInsightChartName: DataInsightChartType.DailyActiveUsers,
dataReportIndex: DataReportIndex.WebAnalyticUserActivityReportDataIndex,
};
const response = await getAggregateChartData(params);
setDailyActiveUsers(response.data ?? []);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchPageViewsByEntities();
}, [chartFilter]);
return (
<Card
className="data-insight-card"
data-testid="entity-active-user-card"
loading={isLoading}
title={
<>
<Typography.Title level={5}>
{t('label.daily-active-user')}
</Typography.Title>
<Typography.Text className="data-insight-label-text">
{t('message.active-users')}
</Typography.Text>
</>
}>
<ResponsiveContainer debounce={1} minHeight={400}>
<LineChart
data={getFormattedActiveUsersData(dailyActiveUsers)}
margin={BAR_CHART_MARGIN}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Line
dataKey="activeUsers"
stroke={DATA_INSIGHT_GRAPH_COLORS[3]}
type="monotone"
/>
</LineChart>
</ResponsiveContainer>
</Card>
);
};
export default DailyActiveUsersChart;

View File

@ -13,6 +13,7 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { act } from 'react-test-renderer';
import DataInsightSummary from './DataInsightSummary'; import DataInsightSummary from './DataInsightSummary';
jest.mock('react-i18next', () => ({ jest.mock('react-i18next', () => ({
@ -21,33 +22,26 @@ jest.mock('react-i18next', () => ({
}), }),
})); }));
const mockFilter = {
startTs: 1667952000000,
endTs: 1668000248671,
};
jest.mock('../../axiosAPIs/DataInsightAPI', () => ({
getAggregateChartData: jest.fn().mockImplementation(() => Promise.resolve()),
}));
describe('Test DataInsightSummary Component', () => { describe('Test DataInsightSummary Component', () => {
it('Should render the overview data', async () => { it('Should render the overview data', async () => {
render(<DataInsightSummary />); await act(async () => {
render(<DataInsightSummary chartFilter={mockFilter} />);
});
const summaryCard = screen.getByTestId('summary-card'); const summaryCard = screen.getByTestId('summary-card');
const allEntityCount = screen.getByTestId('summary-item-All'); const allEntityCount = screen.getByTestId('summary-item-latest');
const usersCount = screen.getByTestId('summary-item-Users');
const sessionCount = screen.getByTestId('summary-item-Sessions');
const activityCount = screen.getByTestId('summary-item-Activity');
const activeUsersCount = screen.getByTestId('summary-item-ActiveUsers');
const tablesCount = screen.getByTestId('summary-item-Tables');
const topicsCount = screen.getByTestId('summary-item-Topics');
const dashboardCount = screen.getByTestId('summary-item-Dashboards');
const mlModelsCount = screen.getByTestId('summary-item-MlModels');
const testCasesCount = screen.getByTestId('summary-item-TestCases');
expect(summaryCard).toBeInTheDocument(); expect(summaryCard).toBeInTheDocument();
expect(allEntityCount).toBeInTheDocument(); expect(allEntityCount).toBeInTheDocument();
expect(usersCount).toBeInTheDocument();
expect(sessionCount).toBeInTheDocument();
expect(activityCount).toBeInTheDocument();
expect(activeUsersCount).toBeInTheDocument();
expect(tablesCount).toBeInTheDocument();
expect(topicsCount).toBeInTheDocument();
expect(dashboardCount).toBeInTheDocument();
expect(mlModelsCount).toBeInTheDocument();
expect(testCasesCount).toBeInTheDocument();
}); });
}); });

View File

@ -12,37 +12,93 @@
*/ */
import { Card, Col, Row, Typography } from 'antd'; import { Card, Col, Row, Typography } from 'antd';
import React from 'react'; import { AxiosError } from 'axios';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { OVERVIEW } from '../../pages/DataInsightPage/DataInsight.mock'; import { getAggregateChartData } from '../../axiosAPIs/DataInsightAPI';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import {
DataInsightChartResult,
DataInsightChartType,
} from '../../generated/dataInsight/dataInsightChartResult';
import { ChartFilter } from '../../interface/data-insight.interface';
import { getGraphDataByEntityType } from '../../utils/DataInsightUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import './DataInsightDetail.less'; import './DataInsightDetail.less';
const DataInsightSummary = () => { interface Props {
chartFilter: ChartFilter;
}
const DataInsightSummary: FC<Props> = ({ chartFilter }) => {
const [totalEntitiesByType, setTotalEntitiesByType] =
useState<DataInsightChartResult>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const { total, latestData = {} } = useMemo(() => {
return getGraphDataByEntityType(
totalEntitiesByType?.data ?? [],
DataInsightChartType.TotalEntitiesByType
);
}, [totalEntitiesByType]);
const { t } = useTranslation(); const { t } = useTranslation();
const fetchTotalEntitiesByType = async () => {
setIsLoading(true);
try {
const params = {
...chartFilter,
dataInsightChartName: DataInsightChartType.TotalEntitiesByType,
dataReportIndex: DataReportIndex.EntityReportDataIndex,
};
const response = await getAggregateChartData(params);
setTotalEntitiesByType(response);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchTotalEntitiesByType();
}, [chartFilter]);
return ( return (
<Card <Card
className="data-insight-card" className="data-insight-card"
data-testid="summary-card" data-testid="summary-card"
loading={isLoading}
title={ title={
<Typography.Title level={5}> <Typography.Title level={5}>
{t('label.data-insight-summary')} {t('label.data-insight-summary')}
</Typography.Title> </Typography.Title>
}> }>
<Row data-testid="summary-card-content" gutter={[16, 16]}> <Row data-testid="summary-card-content" gutter={[16, 16]}>
{OVERVIEW.map((summary, id) => ( <Col data-testid="summary-item-latest" span={4}>
<Col <Typography.Text className="data-insight-label-text">
data-testid={`summary-item-${summary.entityType}`} Latest
key={id} </Typography.Text>
span={4}> <Typography className="font-semibold text-2xl">{total}</Typography>
<Typography.Text className="data-insight-label-text"> </Col>
{summary.entityType} {Object.entries(latestData).map((summary) => {
</Typography.Text> const label = summary[0];
<Typography className="font-semibold text-2xl"> const value = summary[1] as number;
{summary.count}
</Typography> return label !== 'timestamp' ? (
</Col> <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>
</Col>
) : null;
})}
</Row> </Row>
</Card> </Card>
); );

View File

@ -33,6 +33,7 @@ import {
BAR_SIZE, BAR_SIZE,
ENTITIES_BAR_COLO_MAP, ENTITIES_BAR_COLO_MAP,
} from '../../constants/DataInsight.constants'; } from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import { import {
DataInsightChartResult, DataInsightChartResult,
DataInsightChartType, DataInsightChartType,
@ -72,6 +73,7 @@ const DescriptionInsight: FC<Props> = ({ chartFilter }) => {
...chartFilter, ...chartFilter,
dataInsightChartName: dataInsightChartName:
DataInsightChartType.PercentageOfEntitiesWithDescriptionByType, DataInsightChartType.PercentageOfEntitiesWithDescriptionByType,
dataReportIndex: DataReportIndex.EntityReportDataIndex,
}; };
const response = await getAggregateChartData(params); const response = await getAggregateChartData(params);
@ -111,7 +113,7 @@ const DescriptionInsight: FC<Props> = ({ chartFilter }) => {
<XAxis dataKey="timestamp" /> <XAxis dataKey="timestamp" />
<YAxis /> <YAxis />
<Tooltip content={<CustomTooltip />} /> <Tooltip content={<CustomTooltip isPercentage />} />
<Legend <Legend
align="left" align="left"
content={(props) => renderLegend(props as LegendProps, `${total}%`)} content={(props) => renderLegend(props as LegendProps, `${total}%`)}

View File

@ -33,6 +33,7 @@ import {
BAR_SIZE, BAR_SIZE,
ENTITIES_BAR_COLO_MAP, ENTITIES_BAR_COLO_MAP,
} from '../../constants/DataInsight.constants'; } from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import { import {
DataInsightChartResult, DataInsightChartResult,
DataInsightChartType, DataInsightChartType,
@ -72,6 +73,7 @@ const OwnerInsight: FC<Props> = ({ chartFilter }) => {
...chartFilter, ...chartFilter,
dataInsightChartName: dataInsightChartName:
DataInsightChartType.PercentageOfEntitiesWithOwnerByType, DataInsightChartType.PercentageOfEntitiesWithOwnerByType,
dataReportIndex: DataReportIndex.EntityReportDataIndex,
}; };
const response = await getAggregateChartData(params); const response = await getAggregateChartData(params);
@ -107,7 +109,7 @@ const OwnerInsight: FC<Props> = ({ chartFilter }) => {
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" /> <XAxis dataKey="timestamp" />
<YAxis /> <YAxis />
<Tooltip content={<CustomTooltip />} /> <Tooltip content={<CustomTooltip isPercentage />} />
<Legend <Legend
align="left" align="left"
content={(props) => renderLegend(props as LegendProps, `${total}%`)} content={(props) => renderLegend(props as LegendProps, `${total}%`)}

View File

@ -0,0 +1,132 @@
/*
* 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 { Card, Typography } from 'antd';
import { AxiosError } from 'axios';
import { uniqueId } from 'lodash';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Bar,
BarChart,
CartesianGrid,
Legend,
LegendProps,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { getAggregateChartData } from '../../axiosAPIs/DataInsightAPI';
import {
BAR_CHART_MARGIN,
BAR_SIZE,
ENTITIES_BAR_COLO_MAP,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import { DataInsightChartType } from '../../generated/dataInsight/dataInsightChartResult';
import { PageViewsByEntities } from '../../generated/dataInsight/type/pageViewsByEntities';
import { ChartFilter } from '../../interface/data-insight.interface';
import {
CustomTooltip,
getGraphDataByEntityType,
renderLegend,
} from '../../utils/DataInsightUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import './DataInsightDetail.less';
interface Props {
chartFilter: ChartFilter;
}
const PageViewsByEntitiesChart: FC<Props> = ({ chartFilter }) => {
const [pageViewsByEntities, setPageViewsByEntities] =
useState<PageViewsByEntities[]>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const { data, entities, total } = useMemo(() => {
return getGraphDataByEntityType(
pageViewsByEntities,
DataInsightChartType.PageViewsByEntities
);
}, [pageViewsByEntities]);
const { t } = useTranslation();
const fetchPageViewsByEntities = async () => {
setIsLoading(true);
try {
const params = {
...chartFilter,
dataInsightChartName: DataInsightChartType.PageViewsByEntities,
dataReportIndex: DataReportIndex.WebAnalyticEntityViewReportDataIndex,
};
const response = await getAggregateChartData(params);
setPageViewsByEntities(response.data ?? []);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchPageViewsByEntities();
}, [chartFilter]);
return (
<Card
className="data-insight-card"
data-testid="entity-page-views-card"
loading={isLoading}
title={
<>
<Typography.Title level={5}>
{t('label.page-views-by-entities')}
</Typography.Title>
<Typography.Text className="data-insight-label-text">
{t('message.data-insight-page-views')}
</Typography.Text>
</>
}>
<ResponsiveContainer debounce={1} minHeight={400}>
<BarChart data={data} margin={BAR_CHART_MARGIN}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Legend
align="left"
content={(props) => renderLegend(props as LegendProps, `${total}`)}
layout="vertical"
verticalAlign="top"
wrapperStyle={{ left: '0px' }}
/>
{entities.map((entity) => (
<Bar
barSize={BAR_SIZE}
dataKey={entity}
fill={ENTITIES_BAR_COLO_MAP[entity]}
key={uniqueId()}
stackId="entityCount"
/>
))}
</BarChart>
</ResponsiveContainer>
</Card>
);
};
export default PageViewsByEntitiesChart;

View File

@ -33,6 +33,7 @@ import {
BAR_SIZE, BAR_SIZE,
TIER_BAR_COLOR_MAP, TIER_BAR_COLOR_MAP,
} from '../../constants/DataInsight.constants'; } from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import { import {
DataInsightChartResult, DataInsightChartResult,
DataInsightChartType, DataInsightChartType,
@ -68,6 +69,7 @@ const TierInsight: FC<Props> = ({ chartFilter }) => {
const params = { const params = {
...chartFilter, ...chartFilter,
dataInsightChartName: DataInsightChartType.TotalEntitiesByTier, dataInsightChartName: DataInsightChartType.TotalEntitiesByTier,
dataReportIndex: DataReportIndex.EntityReportDataIndex,
}; };
const response = await getAggregateChartData(params); const response = await getAggregateChartData(params);

View File

@ -13,24 +13,59 @@
import { Card, Space, Table, Typography } from 'antd'; import { Card, Space, Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import React, { useMemo } from 'react'; import { AxiosError } from 'axios';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TOP_ACTIVE_USER } from '../../pages/DataInsightPage/DataInsight.mock'; import { Link } from 'react-router-dom';
import { getDateTimeFromMilliSeconds } from '../../utils/TimeUtils'; import { getAggregateChartData } from '../../axiosAPIs/DataInsightAPI';
import { getUserPath } from '../../constants/constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import { DataInsightChartType } from '../../generated/dataInsight/dataInsightChartResult';
import { MostActiveUsers } from '../../generated/dataInsight/type/mostActiveUsers';
import { ChartFilter } from '../../interface/data-insight.interface';
import {
getDateTimeFromMilliSeconds,
getTimeDurationFromSeconds,
} from '../../utils/TimeUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import ProfilePicture from '../common/ProfilePicture/ProfilePicture'; import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
import Loader from '../Loader/Loader';
import './DataInsightDetail.less'; import './DataInsightDetail.less';
interface ActiveUserView {
userName: string; interface Props {
Team: string; chartFilter: ChartFilter;
mostRecentSession: number;
totalSessions: number;
avgSessionDuration: number;
} }
const TopActiveUsers = () => { const TopActiveUsers: FC<Props> = ({ chartFilter }) => {
const [mostActiveUsers, setMostActiveUsers] = useState<MostActiveUsers[]>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const { t } = useTranslation(); const { t } = useTranslation();
const columns: ColumnsType<ActiveUserView> = useMemo( const fetchMostActiveUsers = async () => {
setIsLoading(true);
try {
const params = {
...chartFilter,
dataInsightChartName: DataInsightChartType.MostActiveUsers,
dataReportIndex: DataReportIndex.WebAnalyticUserActivityReportDataIndex,
};
const response = await getAggregateChartData(params);
setMostActiveUsers(response.data ?? []);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchMostActiveUsers();
}, [chartFilter]);
const columns: ColumnsType<MostActiveUsers> = useMemo(
() => [ () => [
{ {
title: t('label.user'), title: t('label.user'),
@ -39,34 +74,34 @@ const TopActiveUsers = () => {
render: (userName: string) => ( render: (userName: string) => (
<Space> <Space>
<ProfilePicture id="" name={userName} type="circle" width="24" /> <ProfilePicture id="" name={userName} type="circle" width="24" />
<Typography.Text>{userName}</Typography.Text> <Link to={getUserPath(userName)}>{userName}</Link>
</Space> </Space>
), ),
}, },
{ {
title: t('label.team'), title: t('label.team'),
dataIndex: 'Team', dataIndex: 'team',
key: 'Team', key: 'team',
render: (Team: string) => ( render: (team: string) => (
<Typography.Text>{Team ?? '--'}</Typography.Text> <Typography.Text>{team ?? '--'}</Typography.Text>
), ),
}, },
{ {
title: t('label.most-recent-session'), title: t('label.most-recent-session'),
dataIndex: 'mostRecentSession', dataIndex: 'lastSession',
key: 'mostRecentSession', key: 'lastSession',
render: (mostRecentSession: number) => ( render: (lastSession: number) => (
<Typography.Text> <Typography.Text>
{getDateTimeFromMilliSeconds(mostRecentSession)} {getDateTimeFromMilliSeconds(lastSession)}
</Typography.Text> </Typography.Text>
), ),
}, },
{ {
title: t('label.total-session'), title: t('label.total-session'),
dataIndex: 'totalSessions', dataIndex: 'sessions',
key: 'totalSessions', key: 'sessions',
render: (totalSessions: number) => ( render: (sessions: number) => (
<Typography.Text>{totalSessions}</Typography.Text> <Typography.Text>{sessions}</Typography.Text>
), ),
}, },
{ {
@ -74,7 +109,9 @@ const TopActiveUsers = () => {
dataIndex: 'avgSessionDuration', dataIndex: 'avgSessionDuration',
key: 'avgSessionDuration', key: 'avgSessionDuration',
render: (avgSessionDuration: number) => ( render: (avgSessionDuration: number) => (
<Typography.Text>{avgSessionDuration}</Typography.Text> <Typography.Text>
{getTimeDurationFromSeconds(avgSessionDuration)}
</Typography.Text>
), ),
}, },
], ],
@ -86,14 +123,20 @@ const TopActiveUsers = () => {
className="data-insight-card" className="data-insight-card"
data-testid="entity-summary-card-percentage" data-testid="entity-summary-card-percentage"
title={ title={
<Typography.Title level={5}> <>
{t('label.data-insight-active-user-summary')} <Typography.Title level={5}>
</Typography.Title> {t('label.data-insight-active-user-summary')}
</Typography.Title>
<Typography.Text className="data-insight-label-text">
{t('message.most-active-users')}
</Typography.Text>
</>
}> }>
<Table <Table
className="data-insight-table-wrapper" className="data-insight-table-wrapper"
columns={columns} columns={columns}
dataSource={TOP_ACTIVE_USER} dataSource={mostActiveUsers}
loading={{ spinning: isLoading, indicator: <Loader /> }}
pagination={false} pagination={false}
size="small" size="small"
/> />

View File

@ -11,81 +11,86 @@
* limitations under the License. * limitations under the License.
*/ */
import { Card, Space, Table, Tag, Typography } from 'antd'; import { Card, Space, Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table'; import { ColumnsType } from 'antd/lib/table';
import React, { useMemo } from 'react'; import { AxiosError } from 'axios';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TOP_VIEW_ENTITIES } from '../../pages/DataInsightPage/DataInsight.mock'; import { getAggregateChartData } from '../../axiosAPIs/DataInsightAPI';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import { DataInsightChartType } from '../../generated/dataInsight/dataInsightChartResult';
import { MostViewedEntities } from '../../generated/dataInsight/type/mostViewedEntities';
import { ChartFilter } from '../../interface/data-insight.interface';
import { getDecodedFqn } from '../../utils/StringsUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import ProfilePicture from '../common/ProfilePicture/ProfilePicture'; import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
import Loader from '../Loader/Loader';
import './DataInsightDetail.less'; import './DataInsightDetail.less';
interface EntityView { interface Props {
entityName: string; chartFilter: ChartFilter;
owner: string;
tags: string[];
entityType: string;
totalViews: number;
uniqueViews: number;
} }
const TopViewEntities = () => { const TopViewEntities: FC<Props> = ({ chartFilter }) => {
const [mostViewedEntities, setMostViewedEntities] =
useState<MostViewedEntities[]>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const { t } = useTranslation(); const { t } = useTranslation();
const columns: ColumnsType<EntityView> = useMemo( const fetchMostViewedEntities = async () => {
setIsLoading(true);
try {
const params = {
...chartFilter,
dataInsightChartName: DataInsightChartType.MostViewedEntities,
dataReportIndex: DataReportIndex.WebAnalyticEntityViewReportDataIndex,
};
const response = await getAggregateChartData(params);
setMostViewedEntities(response.data ?? []);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchMostViewedEntities();
}, [chartFilter]);
const columns: ColumnsType<MostViewedEntities> = useMemo(
() => [ () => [
{ {
title: t('label.entity-name'), title: t('label.entity-name'),
dataIndex: 'entityName', dataIndex: 'entityFqn',
key: 'entityName', key: 'entityName',
render: (entityName: string) => ( render: (entityFqn: string) => (
<Typography.Text>{entityName}</Typography.Text> <Typography.Text>{getDecodedFqn(entityFqn)}</Typography.Text>
), ),
}, },
{ {
title: t('label.owner'), title: t('label.owner'),
dataIndex: 'owner', dataIndex: 'owner',
key: 'owner', key: 'owner',
render: (owner: string) => ( render: (owner: string) =>
<Space> owner ? (
<ProfilePicture id="" name={owner} type="circle" width="24" /> <Space>
<Typography.Text>{owner}</Typography.Text> <ProfilePicture id="" name={owner} type="circle" width="24" />
</Space> <Typography.Text>{owner}</Typography.Text>
), </Space>
}, ) : (
{ <Typography.Text>{t('label.no-owner')}</Typography.Text>
title: t('label.tags'), ),
dataIndex: 'tags',
key: 'tags',
render: (tags: string[]) => (
<Typography.Text>
{tags.map((tag, i) => (
<Tag key={i}>{tag}</Tag>
))}
</Typography.Text>
),
},
{
title: t('label.entity-type'),
dataIndex: 'entityType',
key: 'entityType',
render: (entityType: string) => (
<Typography.Text>{entityType}</Typography.Text>
),
}, },
{ {
title: t('label.total-views'), title: t('label.total-views'),
dataIndex: 'totalViews', dataIndex: 'pageViews',
key: 'totalViews', key: 'totalViews',
render: (totalViews: number) => ( render: (pageViews: number) => (
<Typography.Text>{totalViews}</Typography.Text> <Typography.Text>{pageViews}</Typography.Text>
),
},
{
title: t('label.unique-views'),
dataIndex: 'uniqueViews',
key: 'uniqueViews',
render: (uniqueViews: number) => (
<Typography.Text>{uniqueViews}</Typography.Text>
), ),
}, },
], ],
@ -97,14 +102,20 @@ const TopViewEntities = () => {
className="data-insight-card" className="data-insight-card"
data-testid="entity-summary-card-percentage" data-testid="entity-summary-card-percentage"
title={ title={
<Typography.Title level={5}> <>
{t('label.data-insight-top-viewed-entity-summary')} <Typography.Title level={5}>
</Typography.Title> {t('label.data-insight-top-viewed-entity-summary')}
</Typography.Title>
<Typography.Text className="data-insight-label-text">
{t('message.most-viewed-datasets')}
</Typography.Text>
</>
}> }>
<Table <Table
className="data-insight-table-wrapper" className="data-insight-table-wrapper"
columns={columns} columns={columns}
dataSource={TOP_VIEW_ENTITIES} dataSource={mostViewedEntities}
loading={{ spinning: isLoading, indicator: <Loader /> }}
pagination={false} pagination={false}
size="small" size="small"
/> />

View File

@ -33,6 +33,7 @@ import {
BAR_SIZE, BAR_SIZE,
ENTITIES_BAR_COLO_MAP, ENTITIES_BAR_COLO_MAP,
} from '../../constants/DataInsight.constants'; } from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import { import {
DataInsightChartResult, DataInsightChartResult,
DataInsightChartType, DataInsightChartType,
@ -71,6 +72,7 @@ const TotalEntityInsight: FC<Props> = ({ chartFilter }) => {
const params = { const params = {
...chartFilter, ...chartFilter,
dataInsightChartName: DataInsightChartType.TotalEntitiesByType, dataInsightChartName: DataInsightChartType.TotalEntitiesByType,
dataReportIndex: DataReportIndex.EntityReportDataIndex,
}; };
const response = await getAggregateChartData(params); const response = await getAggregateChartData(params);

View File

@ -11,10 +11,13 @@
* limitations under the License. * limitations under the License.
*/ */
import { TooltipProps } from 'recharts';
import { DataReportIndex } from '../generated/dataInsight/dataInsightChart';
import { DataInsightChartType } from '../generated/dataInsight/dataInsightChartResult'; import { DataInsightChartType } from '../generated/dataInsight/dataInsightChartResult';
export interface ChartAggregateParam { export interface ChartAggregateParam {
dataInsightChartName: DataInsightChartType; dataInsightChartName: DataInsightChartType;
dataReportIndex: DataReportIndex;
startTs: number; startTs: number;
endTs: number; endTs: number;
tier?: string; tier?: string;
@ -27,3 +30,7 @@ export interface ChartFilter {
startTs: number; startTs: number;
endTs: number; endTs: number;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface DataInsightChartTooltipProps extends TooltipProps<any, any> {
isPercentage?: boolean;
}

View File

@ -178,8 +178,8 @@
"data-insight-description-summary": "Percentage of Datasets With Description", "data-insight-description-summary": "Percentage of Datasets With Description",
"data-insight-owner-summary": "Percentage of Datasets With Owners", "data-insight-owner-summary": "Percentage of Datasets With Owners",
"data-insight-tier-summary": "Total Datasets by Tier", "data-insight-tier-summary": "Total Datasets by Tier",
"data-insight-active-user-summary": "Top Active Users", "data-insight-active-user-summary": "Most Active Users",
"data-insight-top-viewed-entity-summary": "Top Viewed Datasets", "data-insight-top-viewed-entity-summary": "Most Viewed Datasets",
"data-insight-total-entity-summary": "Total Datasets", "data-insight-total-entity-summary": "Total Datasets",
"user": "User", "user": "User",
"team": "Team", "team": "Team",
@ -224,6 +224,9 @@
"pipeline": "Pipeline", "pipeline": "Pipeline",
"function": "Function", "function": "Function",
"edge-information": "Edge Information", "edge-information": "Edge Information",
"no-owner": "No Owner",
"page-views-by-entities": "Page views by datasets",
"daily-active-user": "Daily active users on the platform",
"collapse-all": "Collapse All", "collapse-all": "Collapse All",
"expand-all": "Expand All", "expand-all": "Expand All",
"search-lineage": "Search Lineage", "search-lineage": "Search Lineage",
@ -248,7 +251,13 @@
"entity-restored-error": "Error while restoring {{entity}}", "entity-restored-error": "Error while restoring {{entity}}",
"no-ingestion-available": "No ingestion data available", "no-ingestion-available": "No ingestion data available",
"no-ingestion-description": "To view Ingestion Data, run the MetaData Ingestion. Please refer to this doc to schedule the", "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." "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.",
"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."
}, },
"server": { "server": {
"no-followed-entities": "You have not followed anything yet.", "no-followed-entities": "You have not followed anything yet.",
@ -262,8 +271,6 @@
"leave-team-success": "Left the team successfully!", "leave-team-success": "Left the team successfully!",
"join-team-error": "Error while joining the team!", "join-team-error": "Error while joining the team!",
"leave-team-error": "Error while leaving the team!", "leave-team-error": "Error while leaving the team!",
"field-insight": "Display the percentage of datasets with {{field}} by type.",
"total-entity-insight": "Display the total of datasets by type.",
"no-query-available": "No query available", "no-query-available": "No query available",
"unexpected-response": "Unexpected response from server!" "unexpected-response": "Unexpected response from server!"
}, },

View File

@ -27,9 +27,11 @@ import { searchQuery } from '../../axiosAPIs/searchAPI';
import { autocomplete } from '../../components/AdvancedSearch/AdvancedSearch.constants'; import { autocomplete } from '../../components/AdvancedSearch/AdvancedSearch.constants';
import PageLayoutV1 from '../../components/containers/PageLayoutV1'; import PageLayoutV1 from '../../components/containers/PageLayoutV1';
import DailyActiveUsersChart from '../../components/DataInsightDetail/DailyActiveUsersChart';
import DataInsightSummary from '../../components/DataInsightDetail/DataInsightSummary'; import DataInsightSummary from '../../components/DataInsightDetail/DataInsightSummary';
import DescriptionInsight from '../../components/DataInsightDetail/DescriptionInsight'; import DescriptionInsight from '../../components/DataInsightDetail/DescriptionInsight';
import OwnerInsight from '../../components/DataInsightDetail/OwnerInsight'; import OwnerInsight from '../../components/DataInsightDetail/OwnerInsight';
import PageViewsByEntitiesChart from '../../components/DataInsightDetail/PageViewsByEntitiesChart';
import TierInsight from '../../components/DataInsightDetail/TierInsight'; import TierInsight from '../../components/DataInsightDetail/TierInsight';
import TopActiveUsers from '../../components/DataInsightDetail/TopActiveUsers'; import TopActiveUsers from '../../components/DataInsightDetail/TopActiveUsers';
import TopViewEntities from '../../components/DataInsightDetail/TopViewEntities'; import TopViewEntities from '../../components/DataInsightDetail/TopViewEntities';
@ -185,7 +187,7 @@ const DataInsightPage = () => {
</Card> </Card>
</Col> </Col>
<Col span={24}> <Col span={24}>
<DataInsightSummary /> <DataInsightSummary chartFilter={chartFilter} />
</Col> </Col>
<Col span={24}> <Col span={24}>
<Radio.Group <Radio.Group
@ -217,10 +219,16 @@ const DataInsightPage = () => {
{activeTab === DATA_INSIGHT_TAB['Web Analytics'] && ( {activeTab === DATA_INSIGHT_TAB['Web Analytics'] && (
<> <>
<Col span={24}> <Col span={24}>
<TopViewEntities /> <TopViewEntities chartFilter={chartFilter} />
</Col> </Col>
<Col span={24}> <Col span={24}>
<TopActiveUsers /> <PageViewsByEntitiesChart chartFilter={chartFilter} />
</Col>
<Col span={24}>
<TopActiveUsers chartFilter={chartFilter} />
</Col>
<Col span={24}>
<DailyActiveUsersChart chartFilter={chartFilter} />
</Col> </Col>
</> </>
)} )}

View File

@ -80,6 +80,21 @@ jest.mock('../../utils/DataInsightUtils', () => ({
), ),
})); }));
jest.mock('../../components/DataInsightDetail/DailyActiveUsersChart', () =>
jest
.fn()
.mockReturnValue(
<div data-testid="daily-active-users">DailyActiveUsersChart</div>
)
);
jest.mock('../../components/DataInsightDetail/PageViewsByEntitiesChart', () =>
jest
.fn()
.mockReturnValue(
<div data-testid="entities-page-views">PageViewsByEntitiesChart</div>
)
);
describe('Test DataInsightPage Component', () => { describe('Test DataInsightPage Component', () => {
it('Should render all child elements', async () => { it('Should render all child elements', async () => {
render(<DataInsightPage />); render(<DataInsightPage />);

View File

@ -15,12 +15,14 @@ import { Card, Typography } from 'antd';
import { isInteger, last, toNumber } from 'lodash'; import { isInteger, last, toNumber } from 'lodash';
import React from 'react'; import React from 'react';
import { ListItem, ListValues } from 'react-awesome-query-builder'; import { ListItem, ListValues } from 'react-awesome-query-builder';
import { LegendProps, Surface, TooltipProps } from 'recharts'; import { LegendProps, Surface } from 'recharts';
import { import {
DataInsightChartResult, DataInsightChartResult,
DataInsightChartType, DataInsightChartType,
} from '../generated/dataInsight/dataInsightChartResult'; } from '../generated/dataInsight/dataInsightChartResult';
import { DailyActiveUsers } from '../generated/dataInsight/type/dailyActiveUsers';
import { TotalEntitiesByTier } from '../generated/dataInsight/type/totalEntitiesByTier'; import { TotalEntitiesByTier } from '../generated/dataInsight/type/totalEntitiesByTier';
import { DataInsightChartTooltipProps } from '../interface/data-insight.interface';
import { getFormattedDateFromMilliSeconds } from './TimeUtils'; import { getFormattedDateFromMilliSeconds } from './TimeUtils';
export const renderLegend = (legendData: LegendProps, latest: string) => { export const renderLegend = (legendData: LegendProps, latest: string) => {
@ -56,9 +58,11 @@ export const renderLegend = (legendData: LegendProps, latest: string) => {
* we don't have type for Tooltip value and Tooltip * we don't have type for Tooltip value and Tooltip
* that's why we have to use the type "any" * that's why we have to use the type "any"
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const CustomTooltip = (props: TooltipProps<any, any>) => { export const CustomTooltip = (props: DataInsightChartTooltipProps) => {
const { active, payload = [], label } = props; const { active, payload = [], label, isPercentage } = props;
const suffix = isPercentage ? '%' : '';
if (active && payload && payload.length) { if (active && payload && payload.length) {
return ( return (
@ -72,7 +76,9 @@ export const CustomTooltip = (props: TooltipProps<any, any>) => {
</Surface> </Surface>
<span> <span>
{entry.dataKey} -{' '} {entry.dataKey} -{' '}
{isInteger(entry.value) ? entry.value : entry.value?.toFixed(2)} {isInteger(entry.value)
? `${entry.value}${suffix}`
: `${entry.value?.toFixed(2)}${suffix}`}
</span> </span>
</li> </li>
))} ))}
@ -158,6 +164,11 @@ export const getGraphDataByEntityType = (
break; break;
case DataInsightChartType.PageViewsByEntities:
value = data.pageViews;
break;
default: default:
break; break;
} }
@ -178,6 +189,7 @@ export const getGraphDataByEntityType = (
data: graphData, data: graphData,
entities, entities,
total: getLatestCount(latestData), total: getLatestCount(latestData),
latestData,
}; };
}; };
@ -222,3 +234,11 @@ export const getTeamFilter = (suggestionValues: ListValues = []) => {
value: suggestion.value, value: suggestion.value,
})); }));
}; };
export const getFormattedActiveUsersData = (activeUsers: DailyActiveUsers[]) =>
activeUsers.map((user) => ({
...user,
timestamp: user.timestamp
? getFormattedDateFromMilliSeconds(user.timestamp)
: '',
}));

View File

@ -10,8 +10,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { isNil, toNumber } from 'lodash'; import { isNil, toNumber } from 'lodash';
import { DateTime } from 'luxon'; import { DateTime, Duration } from 'luxon';
const msPerSecond = 1000; const msPerSecond = 1000;
const msPerMinute = 60 * msPerSecond; const msPerMinute = 60 * msPerSecond;
@ -336,6 +337,13 @@ export const getFormattedDateFromMilliSeconds = (
export const getDateTimeFromMilliSeconds = (timeStamp: number) => export const getDateTimeFromMilliSeconds = (timeStamp: number) =>
DateTime.fromMillis(timeStamp).toLocaleString(DateTime.DATETIME_MED); DateTime.fromMillis(timeStamp).toLocaleString(DateTime.DATETIME_MED);
/**
* @param seconds EPOCH seconds
* @returns Formatted duration for valid input. Format: 00:09:31
*/
export const getTimeDurationFromSeconds = (seconds: number) =>
!isNil(seconds) ? Duration.fromObject({ seconds }).toFormat('hh:mm:ss') : '';
/** /**
* It takes a timestamp and returns a string in the format of "dd MMM yyyy, hh:mm" * It takes a timestamp and returns a string in the format of "dd MMM yyyy, hh:mm"
* @param {number} timeStamp - number - The timestamp you want to convert to a date. * @param {number} timeStamp - number - The timestamp you want to convert to a date.
@ -350,6 +358,7 @@ export const getDateTimeByTimeStampWithCommaSeparated = (
/** /**
* Given a date string, return the time stamp of that date. * Given a date string, return the time stamp of that date.
* @param {string} date - The date you want to convert to a timestamp. * @param {string} date - The date you want to convert to a timestamp.
* @deprecated
*/ */
export const getTimeStampByDate = (date: string) => Date.parse(date); export const getTimeStampByDate = (date: string) => Date.parse(date);