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,
}
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
Returns:
@ -239,7 +239,7 @@ class WebAnalyticUserActivityReportDataProcessor(DataProcessor):
)
if not user_entity:
return None
return {}
teams = user_entity.teams
return {

View File

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

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 React from 'react';
import { act } from 'react-test-renderer';
import DataInsightSummary from './DataInsightSummary';
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', () => {
it('Should render the overview data', async () => {
render(<DataInsightSummary />);
await act(async () => {
render(<DataInsightSummary chartFilter={mockFilter} />);
});
const summaryCard = screen.getByTestId('summary-card');
const allEntityCount = screen.getByTestId('summary-item-All');
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');
const allEntityCount = screen.getByTestId('summary-item-latest');
expect(summaryCard).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 React from 'react';
import { AxiosError } from 'axios';
import React, { FC, useEffect, useMemo, useState } from 'react';
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';
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 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 (
<Card
className="data-insight-card"
data-testid="summary-card"
loading={isLoading}
title={
<Typography.Title level={5}>
{t('label.data-insight-summary')}
</Typography.Title>
}>
<Row data-testid="summary-card-content" gutter={[16, 16]}>
{OVERVIEW.map((summary, id) => (
<Col
data-testid={`summary-item-${summary.entityType}`}
key={id}
span={4}>
<Typography.Text className="data-insight-label-text">
{summary.entityType}
</Typography.Text>
<Typography className="font-semibold text-2xl">
{summary.count}
</Typography>
</Col>
))}
<Col data-testid="summary-item-latest" span={4}>
<Typography.Text className="data-insight-label-text">
Latest
</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>
</Col>
) : null;
})}
</Row>
</Card>
);

View File

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

View File

@ -33,6 +33,7 @@ import {
BAR_SIZE,
ENTITIES_BAR_COLO_MAP,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import {
DataInsightChartResult,
DataInsightChartType,
@ -72,6 +73,7 @@ const OwnerInsight: FC<Props> = ({ chartFilter }) => {
...chartFilter,
dataInsightChartName:
DataInsightChartType.PercentageOfEntitiesWithOwnerByType,
dataReportIndex: DataReportIndex.EntityReportDataIndex,
};
const response = await getAggregateChartData(params);
@ -107,7 +109,7 @@ const OwnerInsight: FC<Props> = ({ chartFilter }) => {
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" />
<YAxis />
<Tooltip content={<CustomTooltip />} />
<Tooltip content={<CustomTooltip isPercentage />} />
<Legend
align="left"
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,
TIER_BAR_COLOR_MAP,
} from '../../constants/DataInsight.constants';
import { DataReportIndex } from '../../generated/dataInsight/dataInsightChart';
import {
DataInsightChartResult,
DataInsightChartType,
@ -68,6 +69,7 @@ const TierInsight: FC<Props> = ({ chartFilter }) => {
const params = {
...chartFilter,
dataInsightChartName: DataInsightChartType.TotalEntitiesByTier,
dataReportIndex: DataReportIndex.EntityReportDataIndex,
};
const response = await getAggregateChartData(params);

View File

@ -13,24 +13,59 @@
import { Card, Space, Table, Typography } from 'antd';
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 { TOP_ACTIVE_USER } from '../../pages/DataInsightPage/DataInsight.mock';
import { getDateTimeFromMilliSeconds } from '../../utils/TimeUtils';
import { Link } from 'react-router-dom';
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 Loader from '../Loader/Loader';
import './DataInsightDetail.less';
interface ActiveUserView {
userName: string;
Team: string;
mostRecentSession: number;
totalSessions: number;
avgSessionDuration: number;
interface Props {
chartFilter: ChartFilter;
}
const TopActiveUsers = () => {
const TopActiveUsers: FC<Props> = ({ chartFilter }) => {
const [mostActiveUsers, setMostActiveUsers] = useState<MostActiveUsers[]>();
const [isLoading, setIsLoading] = useState<boolean>(false);
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'),
@ -39,34 +74,34 @@ const TopActiveUsers = () => {
render: (userName: string) => (
<Space>
<ProfilePicture id="" name={userName} type="circle" width="24" />
<Typography.Text>{userName}</Typography.Text>
<Link to={getUserPath(userName)}>{userName}</Link>
</Space>
),
},
{
title: t('label.team'),
dataIndex: 'Team',
key: 'Team',
render: (Team: string) => (
<Typography.Text>{Team ?? '--'}</Typography.Text>
dataIndex: 'team',
key: 'team',
render: (team: string) => (
<Typography.Text>{team ?? '--'}</Typography.Text>
),
},
{
title: t('label.most-recent-session'),
dataIndex: 'mostRecentSession',
key: 'mostRecentSession',
render: (mostRecentSession: number) => (
dataIndex: 'lastSession',
key: 'lastSession',
render: (lastSession: number) => (
<Typography.Text>
{getDateTimeFromMilliSeconds(mostRecentSession)}
{getDateTimeFromMilliSeconds(lastSession)}
</Typography.Text>
),
},
{
title: t('label.total-session'),
dataIndex: 'totalSessions',
key: 'totalSessions',
render: (totalSessions: number) => (
<Typography.Text>{totalSessions}</Typography.Text>
dataIndex: 'sessions',
key: 'sessions',
render: (sessions: number) => (
<Typography.Text>{sessions}</Typography.Text>
),
},
{
@ -74,7 +109,9 @@ const TopActiveUsers = () => {
dataIndex: 'avgSessionDuration',
key: 'avgSessionDuration',
render: (avgSessionDuration: number) => (
<Typography.Text>{avgSessionDuration}</Typography.Text>
<Typography.Text>
{getTimeDurationFromSeconds(avgSessionDuration)}
</Typography.Text>
),
},
],
@ -86,14 +123,20 @@ const TopActiveUsers = () => {
className="data-insight-card"
data-testid="entity-summary-card-percentage"
title={
<Typography.Title level={5}>
{t('label.data-insight-active-user-summary')}
</Typography.Title>
<>
<Typography.Title level={5}>
{t('label.data-insight-active-user-summary')}
</Typography.Title>
<Typography.Text className="data-insight-label-text">
{t('message.most-active-users')}
</Typography.Text>
</>
}>
<Table
className="data-insight-table-wrapper"
columns={columns}
dataSource={TOP_ACTIVE_USER}
dataSource={mostActiveUsers}
loading={{ spinning: isLoading, indicator: <Loader /> }}
pagination={false}
size="small"
/>

View File

@ -11,81 +11,86 @@
* 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 React, { useMemo } from 'react';
import { AxiosError } from 'axios';
import React, { FC, useEffect, useMemo, useState } from 'react';
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 Loader from '../Loader/Loader';
import './DataInsightDetail.less';
interface EntityView {
entityName: string;
owner: string;
tags: string[];
entityType: string;
totalViews: number;
uniqueViews: number;
interface Props {
chartFilter: ChartFilter;
}
const TopViewEntities = () => {
const TopViewEntities: FC<Props> = ({ chartFilter }) => {
const [mostViewedEntities, setMostViewedEntities] =
useState<MostViewedEntities[]>();
const [isLoading, setIsLoading] = useState<boolean>(false);
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'),
dataIndex: 'entityName',
dataIndex: 'entityFqn',
key: 'entityName',
render: (entityName: string) => (
<Typography.Text>{entityName}</Typography.Text>
render: (entityFqn: string) => (
<Typography.Text>{getDecodedFqn(entityFqn)}</Typography.Text>
),
},
{
title: t('label.owner'),
dataIndex: 'owner',
key: 'owner',
render: (owner: string) => (
<Space>
<ProfilePicture id="" name={owner} type="circle" width="24" />
<Typography.Text>{owner}</Typography.Text>
</Space>
),
},
{
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>
),
render: (owner: string) =>
owner ? (
<Space>
<ProfilePicture id="" name={owner} type="circle" width="24" />
<Typography.Text>{owner}</Typography.Text>
</Space>
) : (
<Typography.Text>{t('label.no-owner')}</Typography.Text>
),
},
{
title: t('label.total-views'),
dataIndex: 'totalViews',
dataIndex: 'pageViews',
key: 'totalViews',
render: (totalViews: number) => (
<Typography.Text>{totalViews}</Typography.Text>
),
},
{
title: t('label.unique-views'),
dataIndex: 'uniqueViews',
key: 'uniqueViews',
render: (uniqueViews: number) => (
<Typography.Text>{uniqueViews}</Typography.Text>
render: (pageViews: number) => (
<Typography.Text>{pageViews}</Typography.Text>
),
},
],
@ -97,14 +102,20 @@ const TopViewEntities = () => {
className="data-insight-card"
data-testid="entity-summary-card-percentage"
title={
<Typography.Title level={5}>
{t('label.data-insight-top-viewed-entity-summary')}
</Typography.Title>
<>
<Typography.Title level={5}>
{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
className="data-insight-table-wrapper"
columns={columns}
dataSource={TOP_VIEW_ENTITIES}
dataSource={mostViewedEntities}
loading={{ spinning: isLoading, indicator: <Loader /> }}
pagination={false}
size="small"
/>

View File

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

View File

@ -11,10 +11,13 @@
* limitations under the License.
*/
import { TooltipProps } from 'recharts';
import { DataReportIndex } from '../generated/dataInsight/dataInsightChart';
import { DataInsightChartType } from '../generated/dataInsight/dataInsightChartResult';
export interface ChartAggregateParam {
dataInsightChartName: DataInsightChartType;
dataReportIndex: DataReportIndex;
startTs: number;
endTs: number;
tier?: string;
@ -27,3 +30,7 @@ export interface ChartFilter {
startTs: 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-owner-summary": "Percentage of Datasets With Owners",
"data-insight-tier-summary": "Total Datasets by Tier",
"data-insight-active-user-summary": "Top Active Users",
"data-insight-top-viewed-entity-summary": "Top Viewed Datasets",
"data-insight-active-user-summary": "Most Active Users",
"data-insight-top-viewed-entity-summary": "Most Viewed Datasets",
"data-insight-total-entity-summary": "Total Datasets",
"user": "User",
"team": "Team",
@ -224,6 +224,9 @@
"pipeline": "Pipeline",
"function": "Function",
"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",
"expand-all": "Expand All",
"search-lineage": "Search Lineage",
@ -248,7 +251,13 @@
"entity-restored-error": "Error while restoring {{entity}}",
"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",
"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": {
"no-followed-entities": "You have not followed anything yet.",
@ -262,8 +271,6 @@
"leave-team-success": "Left the team successfully!",
"join-team-error": "Error while joining 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",
"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 PageLayoutV1 from '../../components/containers/PageLayoutV1';
import DailyActiveUsersChart from '../../components/DataInsightDetail/DailyActiveUsersChart';
import DataInsightSummary from '../../components/DataInsightDetail/DataInsightSummary';
import DescriptionInsight from '../../components/DataInsightDetail/DescriptionInsight';
import OwnerInsight from '../../components/DataInsightDetail/OwnerInsight';
import PageViewsByEntitiesChart from '../../components/DataInsightDetail/PageViewsByEntitiesChart';
import TierInsight from '../../components/DataInsightDetail/TierInsight';
import TopActiveUsers from '../../components/DataInsightDetail/TopActiveUsers';
import TopViewEntities from '../../components/DataInsightDetail/TopViewEntities';
@ -185,7 +187,7 @@ const DataInsightPage = () => {
</Card>
</Col>
<Col span={24}>
<DataInsightSummary />
<DataInsightSummary chartFilter={chartFilter} />
</Col>
<Col span={24}>
<Radio.Group
@ -217,10 +219,16 @@ const DataInsightPage = () => {
{activeTab === DATA_INSIGHT_TAB['Web Analytics'] && (
<>
<Col span={24}>
<TopViewEntities />
<TopViewEntities chartFilter={chartFilter} />
</Col>
<Col span={24}>
<TopActiveUsers />
<PageViewsByEntitiesChart chartFilter={chartFilter} />
</Col>
<Col span={24}>
<TopActiveUsers chartFilter={chartFilter} />
</Col>
<Col span={24}>
<DailyActiveUsersChart chartFilter={chartFilter} />
</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', () => {
it('Should render all child elements', async () => {
render(<DataInsightPage />);

View File

@ -15,12 +15,14 @@ import { Card, Typography } from 'antd';
import { isInteger, last, toNumber } from 'lodash';
import React from 'react';
import { ListItem, ListValues } from 'react-awesome-query-builder';
import { LegendProps, Surface, TooltipProps } from 'recharts';
import { LegendProps, Surface } from 'recharts';
import {
DataInsightChartResult,
DataInsightChartType,
} from '../generated/dataInsight/dataInsightChartResult';
import { DailyActiveUsers } from '../generated/dataInsight/type/dailyActiveUsers';
import { TotalEntitiesByTier } from '../generated/dataInsight/type/totalEntitiesByTier';
import { DataInsightChartTooltipProps } from '../interface/data-insight.interface';
import { getFormattedDateFromMilliSeconds } from './TimeUtils';
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
* 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>) => {
const { active, payload = [], label } = props;
export const CustomTooltip = (props: DataInsightChartTooltipProps) => {
const { active, payload = [], label, isPercentage } = props;
const suffix = isPercentage ? '%' : '';
if (active && payload && payload.length) {
return (
@ -72,7 +76,9 @@ export const CustomTooltip = (props: TooltipProps<any, any>) => {
</Surface>
<span>
{entry.dataKey} -{' '}
{isInteger(entry.value) ? entry.value : entry.value?.toFixed(2)}
{isInteger(entry.value)
? `${entry.value}${suffix}`
: `${entry.value?.toFixed(2)}${suffix}`}
</span>
</li>
))}
@ -158,6 +164,11 @@ export const getGraphDataByEntityType = (
break;
case DataInsightChartType.PageViewsByEntities:
value = data.pageViews;
break;
default:
break;
}
@ -178,6 +189,7 @@ export const getGraphDataByEntityType = (
data: graphData,
entities,
total: getLatestCount(latestData),
latestData,
};
};
@ -222,3 +234,11 @@ export const getTeamFilter = (suggestionValues: ListValues = []) => {
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
* limitations under the License.
*/
import { isNil, toNumber } from 'lodash';
import { DateTime } from 'luxon';
import { DateTime, Duration } from 'luxon';
const msPerSecond = 1000;
const msPerMinute = 60 * msPerSecond;
@ -336,6 +337,13 @@ export const getFormattedDateFromMilliSeconds = (
export const getDateTimeFromMilliSeconds = (timeStamp: number) =>
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"
* @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.
* @param {string} date - The date you want to convert to a timestamp.
* @deprecated
*/
export const getTimeStampByDate = (date: string) => Date.parse(date);