mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-27 10:26:09 +00:00
* 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:
parent
b4e5f6ec13
commit
43ea44a0f9
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -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;
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}%`)}
|
||||
|
@ -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}%`)}
|
||||
|
@ -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;
|
@ -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);
|
||||
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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!"
|
||||
},
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
@ -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 />);
|
||||
|
@ -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)
|
||||
: '',
|
||||
}));
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user