Improvement: #21455 Improve renderning time for profiler chart for large amount of data (#21525)

* Improvement: #21455 Improve renderning time for profiler chart for large amount of data

* Update Profiler and Chart components to handle larger datasets by increasing data size limit and removing unnecessary dataKey prop from Brush component.

* addressing comment
This commit is contained in:
Shailesh Parmar 2025-06-03 19:18:50 +05:30
parent 0a3c938b67
commit f9df0793d6
14 changed files with 459 additions and 167 deletions

View File

@ -41,6 +41,7 @@ export interface ProfilerDetailsCardProps {
curveType?: CurveType;
isLoading?: boolean;
noDataPlaceholderText?: ReactNode;
children?: ReactNode;
}
export enum TableProfilerTab {

View File

@ -13,6 +13,7 @@
import { queryByAttribute, render, screen } from '@testing-library/react';
import React from 'react';
import '../../../../test/unit/mocks/recharts.mock';
import { ProfilerDetailsCardProps } from '../ProfilerDashboard/profilerDashboard.interface';
import ProfilerDetailsCard from './ProfilerDetailsCard';
@ -39,6 +40,11 @@ jest.mock('../../../common/ErrorWithPlaceholder/ErrorPlaceHolder', () =>
jest.mock('../../../../utils/DataInsightUtils', () => ({
CustomTooltip: jest.fn(() => <div>CustomTooltip</div>),
}));
jest.mock('../../../../constants/profiler.constant', () => {
return {
PROFILER_CHART_DATA_SIZE: 500,
};
});
// Improve mock data to be minimal
const mockProps: ProfilerDetailsCardProps = {
@ -49,6 +55,11 @@ const mockProps: ProfilerDetailsCardProps = {
name: 'rowCount',
};
const mockData = Array.from({ length: 501 }, (_, index) => ({
name: `test ${index}`,
value: index,
}));
describe('ProfilerDetailsCard Test', () => {
it('Component should render', async () => {
const { container } = render(<ProfilerDetailsCard {...mockProps} />);
@ -59,6 +70,21 @@ describe('ProfilerDetailsCard Test', () => {
expect(
queryByAttribute('id', container, `${mockProps.name}_graph`)
).toBeInTheDocument();
expect(screen.queryByText('Brush')).not.toBeInTheDocument();
});
it('Component should render brush when data length is greater than PROFILER_CHART_DATA_SIZE', async () => {
render(
<ProfilerDetailsCard
{...mockProps}
chartCollection={{
data: mockData,
information: mockProps.chartCollection.information,
}}
/>
);
expect(screen.getByText('Brush')).toBeInTheDocument();
});
it('No data should be rendered', async () => {

View File

@ -12,8 +12,9 @@
*/
import { Card, Col, Row, Typography } from 'antd';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import {
Brush,
CartesianGrid,
Legend,
LegendProps,
@ -25,6 +26,7 @@ import {
YAxis,
} from 'recharts';
import { GRAPH_BACKGROUND_COLOR } from '../../../../constants/constants';
import { PROFILER_CHART_DATA_SIZE } from '../../../../constants/profiler.constant';
import {
axisTickFormatter,
tooltipFormatter,
@ -48,6 +50,12 @@ const ProfilerDetailsCard: React.FC<ProfilerDetailsCardProps> = ({
}: ProfilerDetailsCardProps) => {
const { data, information } = chartCollection;
const [activeKeys, setActiveKeys] = useState<string[]>([]);
const { showBrush, endIndex } = useMemo(() => {
return {
showBrush: data.length > PROFILER_CHART_DATA_SIZE,
endIndex: PROFILER_CHART_DATA_SIZE,
};
}, [data]);
const handleClick: LegendProps['onClick'] = (event) => {
setActiveKeys((prevActiveKeys) =>
@ -122,6 +130,15 @@ const ProfilerDetailsCard: React.FC<ProfilerDetailsCardProps> = ({
/>
))}
<Legend onClick={handleClick} />
{showBrush && (
<Brush
data={data}
endIndex={endIndex}
gap={5}
height={30}
padding={{ left: 16, right: 16 }}
/>
)}
</LineChart>
</ResponsiveContainer>
) : (

View File

@ -0,0 +1,43 @@
/*
* Copyright 2025 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, Col, Row, Typography } from 'antd';
import React from 'react';
import ProfilerLatestValue from '../ProfilerLatestValue/ProfilerLatestValue';
import { ProfilerStateWrapperProps } from './ProfilerStateWrapper.interface';
const ProfilerStateWrapper = ({
isLoading,
children,
title,
profilerLatestValueProps,
dataTestId,
}: ProfilerStateWrapperProps) => {
return (
<Card
className="shadow-none global-border-radius"
data-testid={dataTestId ?? 'profiler-details-card-container'}
loading={isLoading}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Typography.Title level={5}>{title}</Typography.Title>
</Col>
<Col span={4}>
<ProfilerLatestValue {...profilerLatestValueProps} />
</Col>
<Col span={20}>{children}</Col>
</Row>
</Card>
);
};
export default ProfilerStateWrapper;

View File

@ -0,0 +1,21 @@
/*
* Copyright 2025 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 { ProfilerLatestValueProps } from '../ProfilerDashboard/profilerDashboard.interface';
export interface ProfilerStateWrapperProps {
isLoading: boolean;
children: React.ReactNode;
title: string;
profilerLatestValueProps: ProfilerLatestValueProps;
dataTestId?: string;
}

View File

@ -0,0 +1,101 @@
/*
* Copyright 2025 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 { render, screen } from '@testing-library/react';
import React from 'react';
import ProfilerStateWrapper from './ProfilerStateWrapper.component';
// Mock ProfilerLatestValue component
jest.mock('../ProfilerLatestValue/ProfilerLatestValue', () => {
return jest.fn(() => (
<div data-testid="profiler-latest-value">ProfilerLatestValue</div>
));
});
describe('ProfilerStateWrapper', () => {
const mockProfilerLatestValueProps = {
information: [
{
title: 'Test Label',
dataKey: 'testKey',
color: '#000000',
latestValue: '100',
},
],
};
const defaultProps = {
isLoading: false,
title: 'Test Title',
profilerLatestValueProps: mockProfilerLatestValueProps,
children: <div>Test Content</div>,
};
it('renders without crashing', () => {
render(<ProfilerStateWrapper {...defaultProps} />);
expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText('Test Content')).toBeInTheDocument();
});
it('shows loading state when isLoading is true', () => {
render(<ProfilerStateWrapper {...defaultProps} isLoading />);
const card = screen.getByTestId('profiler-details-card-container');
expect(card).toHaveClass('ant-card-loading');
});
it('renders with custom data-testid', () => {
const customTestId = 'custom-test-id';
render(
<ProfilerStateWrapper {...defaultProps} dataTestId={customTestId} />
);
expect(screen.getByTestId(customTestId)).toBeInTheDocument();
});
it('renders with default data-testid when not provided', () => {
render(<ProfilerStateWrapper {...defaultProps} />);
expect(
screen.getByTestId('profiler-details-card-container')
).toBeInTheDocument();
});
it('renders ProfilerLatestValue with correct props', () => {
render(<ProfilerStateWrapper {...defaultProps} />);
const profilerLatestValue = screen.getByTestId('profiler-latest-value');
expect(profilerLatestValue).toBeInTheDocument();
});
it('renders children content correctly', () => {
const { title, profilerLatestValueProps, isLoading } = defaultProps;
render(
<ProfilerStateWrapper
isLoading={isLoading}
profilerLatestValueProps={profilerLatestValueProps}
title={title}>
<div>
<h1>Custom Header</h1>
<p>Custom Paragraph</p>
</div>
</ProfilerStateWrapper>
);
expect(screen.getByText('Custom Header')).toBeInTheDocument();
expect(screen.getByText('Custom Paragraph')).toBeInTheDocument();
});
});

View File

@ -12,21 +12,12 @@
*/
import { DownOutlined } from '@ant-design/icons';
import {
Button,
Card,
Col,
Dropdown,
Row,
Space,
Tooltip,
Typography,
} from 'antd';
import { Button, Col, Dropdown, Row, Space, Tooltip } from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { isEqual, pick } from 'lodash';
import { DateRangeObject } from 'Models';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { ReactComponent as SettingIcon } from '../../../../../assets/svg/ic-settings-primery.svg';
@ -64,7 +55,7 @@ import CustomBarChart from '../../../../Visualisations/Chart/CustomBarChart';
import OperationDateBarChart from '../../../../Visualisations/Chart/OperationDateBarChart';
import { MetricChartType } from '../../ProfilerDashboard/profilerDashboard.interface';
import ProfilerDetailsCard from '../../ProfilerDetailsCard/ProfilerDetailsCard';
import ProfilerLatestValue from '../../ProfilerLatestValue/ProfilerLatestValue';
import ProfilerStateWrapper from '../../ProfilerStateWrapper/ProfilerStateWrapper.component';
import CustomMetricGraphs from '../CustomMetricGraphs/CustomMetricGraphs.component';
import NoProfilerBanner from '../NoProfilerBanner/NoProfilerBanner.component';
import { TableProfilerChartProps } from '../TableProfiler.interface';
@ -104,7 +95,8 @@ const TableProfilerChart = ({
);
const [operationDateMetrics, setOperationDateMetrics] =
useState<MetricChartType>(INITIAL_OPERATION_METRIC_VALUE);
const [isLoading, setIsLoading] = useState(true);
const [isTableProfilerLoading, setIsTableProfilerLoading] = useState(true);
const [isSystemProfilerLoading, setIsSystemProfilerLoading] = useState(true);
const [profileMetrics, setProfileMetrics] = useState<TableProfile[]>([]);
const profilerDocsLink =
documentationLinksClassBase.getDocsURLS()
@ -163,53 +155,95 @@ const TableProfilerChart = ({
}
};
const fetchTableProfiler = async (
fqn: string,
dateRangeObj: DateRangeObject
) => {
try {
const { data } = await getTableProfilesList(fqn, dateRangeObj);
const rowMetricsData = calculateRowCountMetrics(data, rowCountMetrics);
setRowCountMetrics(rowMetricsData);
setProfileMetrics(data);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const fetchSystemProfiler = async (
fqn: string,
dateRangeObj: DateRangeObject
) => {
try {
const { data } = await getSystemProfileList(fqn, dateRangeObj);
const { operationMetrics: metricsData, operationDateMetrics } =
calculateSystemMetrics(data, operationMetrics);
const fetchTableProfiler = useCallback(
async (fqn: string, dateRangeObj: DateRangeObject) => {
setIsTableProfilerLoading(true);
try {
const { data } = await getTableProfilesList(fqn, dateRangeObj);
const rowMetricsData = calculateRowCountMetrics(data, rowCountMetrics);
setRowCountMetrics(rowMetricsData);
setProfileMetrics(data);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsTableProfilerLoading(false);
}
},
[rowCountMetrics, profileMetrics]
);
const fetchSystemProfiler = useCallback(
async (fqn: string, dateRangeObj: DateRangeObject) => {
setIsSystemProfilerLoading(true);
try {
const { data } = await getSystemProfileList(fqn, dateRangeObj);
const { operationMetrics: metricsData, operationDateMetrics } =
calculateSystemMetrics(data, operationMetrics);
setOperationDateMetrics(operationDateMetrics);
setOperationMetrics(metricsData);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
setOperationDateMetrics(operationDateMetrics);
setOperationMetrics(metricsData);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsSystemProfilerLoading(false);
}
},
[operationMetrics, operationDateMetrics]
);
const fetchProfilerData = async (
fqn: string,
dateRangeObj: DateRangeObject
) => {
const dateRange = pick(dateRangeObj, ['startTs', 'endTs']);
setIsLoading(true);
await fetchTableProfiler(fqn, dateRange);
await fetchSystemProfiler(fqn, dateRange);
setIsLoading(false);
};
const fetchProfilerData = useCallback(
(fqn: string, dateRangeObj: DateRangeObject) => {
const dateRange = pick(dateRangeObj, ['startTs', 'endTs']);
fetchTableProfiler(fqn, dateRange);
fetchSystemProfiler(fqn, dateRange);
},
[fetchSystemProfiler, fetchTableProfiler]
);
useEffect(() => {
if (datasetFQN || entityFqn) {
fetchProfilerData(datasetFQN || entityFqn, dateRangeObject);
} else {
setIsLoading(false);
setIsTableProfilerLoading(false);
setIsSystemProfilerLoading(false);
}
}, [datasetFQN, dateRangeObject]);
}, [datasetFQN, dateRangeObject, entityFqn]);
const operationDateMetricsCard = useMemo(() => {
return (
<ProfilerStateWrapper
dataTestId="operation-date-metrics"
isLoading={isSystemProfilerLoading}
profilerLatestValueProps={{
information: operationDateMetrics.information,
stringValue: true,
}}
title={t('label.table-update-plural')}>
<OperationDateBarChart
chartCollection={operationDateMetrics}
name="operationDateMetrics"
noDataPlaceholderText={noProfilerMessage}
/>
</ProfilerStateWrapper>
);
}, [isSystemProfilerLoading, operationDateMetrics, noProfilerMessage]);
const operationMetricsCard = useMemo(() => {
return (
<ProfilerStateWrapper
dataTestId="operation-metrics"
isLoading={isSystemProfilerLoading}
profilerLatestValueProps={{
information: operationMetrics.information,
}}
title={t('label.volume-change')}>
<CustomBarChart
chartCollection={operationMetrics}
name="operationMetrics"
noDataPlaceholderText={noProfilerMessage}
/>
</ProfilerStateWrapper>
);
}, [isSystemProfilerLoading, operationMetrics, noProfilerMessage]);
return (
<Row data-testid="table-profiler-chart-container" gutter={[16, 16]}>
@ -290,68 +324,19 @@ const TableProfilerChart = ({
<ProfilerDetailsCard
chartCollection={rowCountMetrics}
curveType="stepAfter"
isLoading={isLoading}
isLoading={isTableProfilerLoading}
name="rowCount"
noDataPlaceholderText={noProfilerMessage}
title={t('label.data-volume')}
/>
</Col>
<Col span={24}>
<Card
className="shadow-none global-border-radius"
data-testid="operation-date-metrics"
loading={isLoading}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Typography.Title level={5}>
{t('label.table-update-plural')}
</Typography.Title>
</Col>
<Col span={4}>
<ProfilerLatestValue
stringValue
information={operationDateMetrics.information}
/>
</Col>
<Col span={20}>
<OperationDateBarChart
chartCollection={operationDateMetrics}
name="operationDateMetrics"
noDataPlaceholderText={noProfilerMessage}
/>
</Col>
</Row>
</Card>
</Col>
<Col span={24}>
<Card
className="shadow-none global-border-radius"
data-testid="operation-metrics"
loading={isLoading}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Typography.Title level={5}>
{t('label.volume-change')}
</Typography.Title>
</Col>
<Col span={4}>
<ProfilerLatestValue information={operationMetrics.information} />
</Col>
<Col span={20}>
<CustomBarChart
chartCollection={operationMetrics}
name="operationMetrics"
noDataPlaceholderText={noProfilerMessage}
/>
</Col>
</Row>
</Card>
</Col>
<Col span={24}>{operationDateMetricsCard}</Col>
<Col span={24}>{operationMetricsCard}</Col>
<Col span={24}>
<CustomMetricGraphs
customMetrics={customMetrics}
customMetricsGraphData={tableCustomMetricsProfiling}
isLoading={isLoading || isSummaryLoading}
isLoading={isTableProfilerLoading || isSummaryLoading}
/>
</Col>
</Row>

View File

@ -42,6 +42,11 @@ jest.mock('../../../utils/DataInsightUtils', () => {
});
});
const mockData = Array.from({ length: 501 }, (_, index) => ({
name: `test ${index}`,
value: index,
}));
jest.mock('../../../utils/date-time/DateTimeUtils', () => ({
formatDateTimeLong: jest.fn(),
}));
@ -51,6 +56,10 @@ jest.mock('../../common/ErrorWithPlaceholder/ErrorPlaceHolder', () => ({
default: jest.fn().mockReturnValue(<div>ErrorPlaceHolder</div>),
}));
jest.mock('../../../constants/profiler.constant', () => ({
PROFILER_CHART_DATA_SIZE: 500,
}));
jest.mock('../../../utils/ChartUtils', () => ({
axisTickFormatter: jest.fn(),
tooltipFormatter: jest.fn(),
@ -70,6 +79,21 @@ describe('CustomBarChart component test', () => {
expect(XAxis).toBeInTheDocument();
expect(YAxis).toBeInTheDocument();
expect(noData).not.toBeInTheDocument();
expect(screen.queryByText('Brush')).not.toBeInTheDocument();
});
it('Component should render brush when data length is greater than PROFILER_CHART_DATA_SIZE', async () => {
render(
<CustomBarChart
{...mockCustomBarChartProp}
chartCollection={{
data: mockData,
information: mockCustomBarChartProp.chartCollection.information,
}}
/>
);
expect(screen.getByText('Brush')).toBeInTheDocument();
});
it('If there is no data, placeholder should be visible', async () => {

View File

@ -12,10 +12,11 @@
*/
import { Col, Row } from 'antd';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import {
Bar,
BarChart,
Brush,
CartesianGrid,
Legend,
LegendProps,
@ -25,6 +26,7 @@ import {
YAxis,
} from 'recharts';
import { GRAPH_BACKGROUND_COLOR } from '../../../constants/constants';
import { PROFILER_CHART_DATA_SIZE } from '../../../constants/profiler.constant';
import {
axisTickFormatter,
tooltipFormatter,
@ -44,6 +46,13 @@ const CustomBarChart = ({
const { data, information } = chartCollection;
const [activeKeys, setActiveKeys] = useState<string[]>([]);
const { showBrush, endIndex } = useMemo(() => {
return {
showBrush: data.length > PROFILER_CHART_DATA_SIZE,
endIndex: PROFILER_CHART_DATA_SIZE,
};
}, [data.length]);
if (data.length === 0) {
return (
<Row align="middle" className="h-full w-full" justify="center">
@ -104,6 +113,15 @@ const CustomBarChart = ({
/>
))}
<Legend onClick={handleClick} />
{showBrush && (
<Brush
data={data}
endIndex={endIndex}
gap={5}
height={30}
padding={{ left: 16, right: 16 }}
/>
)}
</BarChart>
</ResponsiveContainer>
);

View File

@ -42,6 +42,11 @@ jest.mock('../../../utils/DataInsightUtils', () => {
});
});
const mockData = Array.from({ length: 501 }, (_, index) => ({
name: `test ${index}`,
value: index,
}));
jest.mock('../../../utils/ChartUtils', () => ({
tooltipFormatter: jest.fn(),
updateActiveChartFilter: jest.fn(),
@ -50,6 +55,9 @@ jest.mock('../../../utils/ChartUtils', () => ({
jest.mock('../../../utils/date-time/DateTimeUtils', () => ({
formatDateTimeLong: jest.fn(),
}));
jest.mock('../../../constants/profiler.constant', () => ({
PROFILER_CHART_DATA_SIZE: 500,
}));
jest.mock('../../common/ErrorWithPlaceholder/ErrorPlaceHolder', () => ({
__esModule: true,
@ -67,8 +75,23 @@ describe('OperationDateBarChart component test', () => {
expect(container).toBeInTheDocument();
expect(XAxis).toBeInTheDocument();
expect(YAxis).not.toBeInTheDocument();
expect(YAxis).toBeInTheDocument();
expect(noData).not.toBeInTheDocument();
expect(screen.queryByText('Brush')).not.toBeInTheDocument();
});
it('Component should render brush when data length is greater than PROFILER_CHART_DATA_SIZE', async () => {
render(
<OperationDateBarChart
{...mockCustomBarChartProp}
chartCollection={{
data: mockData,
information: mockCustomBarChartProp.chartCollection.information,
}}
/>
);
expect(screen.getByText('Brush')).toBeInTheDocument();
});
it('If there is no data, placeholder should be visible', async () => {

View File

@ -12,9 +12,10 @@
*/
import { Col, Row } from 'antd';
import React, { useState } from 'react';
import React, { Fragment, useMemo, useState } from 'react';
import {
Bar,
Brush,
CartesianGrid,
ComposedChart,
Legend,
@ -23,8 +24,10 @@ import {
Scatter,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { GRAPH_BACKGROUND_COLOR } from '../../../constants/constants';
import { PROFILER_CHART_DATA_SIZE } from '../../../constants/profiler.constant';
import {
tooltipFormatter,
updateActiveChartFilter,
@ -42,6 +45,13 @@ const OperationDateBarChart = ({
const { data, information } = chartCollection;
const [activeKeys, setActiveKeys] = useState<string[]>([]);
const { showBrush, endIndex } = useMemo(() => {
return {
showBrush: data.length > PROFILER_CHART_DATA_SIZE,
endIndex: PROFILER_CHART_DATA_SIZE,
};
}, [data.length]);
const handleClick: LegendProps['onClick'] = (event) => {
setActiveKeys((prevActiveKeys) =>
updateActiveChartFilter(event.dataKey, prevActiveKeys)
@ -73,6 +83,14 @@ const OperationDateBarChart = ({
padding={{ left: 16, right: 16 }}
tick={{ fontSize: 12 }}
/>
<YAxis
allowDataOverflow
padding={{ top: 16, bottom: 16 }}
tick={{ fontSize: 12 }}
// need to show empty string to hide the tick value, to align the chart with other charts
tickFormatter={() => ''}
tickLine={false}
/>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} />
<Tooltip
content={
@ -86,30 +104,39 @@ const OperationDateBarChart = ({
/>
{information.map((info) => (
<Bar
barSize={1}
dataKey={info.dataKey}
fill={info.color}
hide={
activeKeys.length ? !activeKeys.includes(info.dataKey) : false
}
key={`${info.dataKey}-bar`}
name={info.title}
stackId="data"
/>
))}
{information.map((info) => (
<Scatter
dataKey={info.dataKey}
fill={info.color}
hide={
activeKeys.length ? !activeKeys.includes(info.dataKey) : false
}
key={`${info.dataKey}-scatter`}
name={info.title}
/>
<Fragment key={info.dataKey}>
<Bar
barSize={1}
dataKey={info.dataKey}
fill={info.color}
hide={
activeKeys.length ? !activeKeys.includes(info.dataKey) : false
}
key={`${info.dataKey}-bar`}
name={info.title}
stackId="data"
/>
<Scatter
dataKey={info.dataKey}
fill={info.color}
hide={
activeKeys.length ? !activeKeys.includes(info.dataKey) : false
}
key={`${info.dataKey}-scatter`}
name={info.title}
/>
</Fragment>
))}
<Legend payloadUniqBy onClick={handleClick} />
{showBrush && (
<Brush
data={data}
endIndex={endIndex}
gap={5}
height={30}
padding={{ left: 16, right: 16 }}
/>
)}
</ComposedChart>
</ResponsiveContainer>
);

View File

@ -64,6 +64,7 @@ export const PROFILER_METRIC = [
'histogram',
'customMetricsProfile',
];
export const PROFILER_CHART_DATA_SIZE = 500;
export const PROFILER_FILTER_RANGE: DateFilterType = {
yesterday: {

View File

@ -14,6 +14,8 @@
import React from 'react';
jest.mock('recharts', () => ({
Bar: jest.fn().mockImplementation(() => <div>Bar</div>),
Line: jest.fn().mockImplementation(() => <div>Line</div>),
Brush: jest.fn().mockImplementation(() => <div>Brush</div>),
Area: jest.fn().mockImplementation(() => <div>Area</div>),
Scatter: jest.fn().mockImplementation(() => <div>Scatter</div>),
CartesianGrid: jest.fn().mockImplementation(() => <div>CartesianGrid</div>),
@ -27,6 +29,9 @@ jest.mock('recharts', () => ({
AreaChart: jest
.fn()
.mockImplementation(({ children }) => <div>{children}</div>),
LineChart: jest
.fn()
.mockImplementation(({ children }) => <div>{children}</div>),
ComposedChart: jest
.fn()
.mockImplementation(({ children }) => <div>{children}</div>),
@ -35,9 +40,9 @@ jest.mock('recharts', () => ({
.mockImplementation(({ children, ...rest }) => (
<div {...rest}>{children}</div>
)),
ResponsiveContainer: jest
.fn()
.mockImplementation(({ children }) => (
<div data-testid="responsive-container">{children}</div>
)),
ResponsiveContainer: jest.fn().mockImplementation(({ children, ...rest }) => (
<div data-testid="responsive-container" {...rest}>
{children}
</div>
)),
}));

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { findLast, isUndefined, last, sortBy } from 'lodash';
import { isUndefined, last, sortBy } from 'lodash';
import { MetricChartType } from '../components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface';
import { SystemProfile } from '../generated/api/data/createTableProfile';
import { Table, TableProfile } from '../generated/entity/data/table';
@ -30,10 +30,11 @@ export const calculateRowCountMetrics = (
profiler: TableProfile[],
currentMetrics: MetricChartType
): MetricChartType => {
const updateProfilerData = sortBy(profiler, 'timestamp');
const rowCountMetricData: MetricChartType['data'] = [];
updateProfilerData.forEach((data) => {
// reverse the profiler data to show the latest data at the top
for (let i = profiler.length - 1; i >= 0; i--) {
const data = profiler[i];
const timestamp = customFormatDateTime(
data.timestamp,
DATE_TIME_12_HOUR_FORMAT
@ -44,7 +45,8 @@ export const calculateRowCountMetrics = (
timestamp: data.timestamp,
rowCount: data.rowCount,
});
});
}
const countMetricInfo = currentMetrics.information.map((item) => ({
...item,
latestValue:
@ -59,16 +61,23 @@ export const calculateSystemMetrics = (
currentMetrics: MetricChartType,
stackId?: string
) => {
const updateProfilerData = sortBy(profiler, 'timestamp');
const operationMetrics: MetricChartType['data'] = [];
const operationDateMetrics: MetricChartType['data'] = [];
const latestOperations = new Map<string, SystemProfile>();
updateProfilerData.forEach((data) => {
// reverse the profiler data to show the latest data at the top
for (let i = profiler.length - 1; i >= 0; i--) {
const data = profiler[i];
const timestamp = customFormatDateTime(
data.timestamp,
DATE_TIME_12_HOUR_FORMAT
);
// Store latest operation if not already stored
if (data.operation) {
latestOperations.set(data.operation, data);
}
operationMetrics.push({
name: timestamp,
timestamp: Number(data.timestamp),
@ -80,33 +89,24 @@ export const calculateSystemMetrics = (
data: data.rowsAffected,
[data.operation ?? 'value']: 5,
});
});
const operationMetricsInfo = currentMetrics.information.map((item) => {
const operation = findLast(
updateProfilerData,
(value) => value.operation === item.dataKey
);
}
return {
...item,
stackId: stackId,
latestValue: operation?.rowsAffected,
};
});
const operationDateMetricsInfo = currentMetrics.information.map((item) => {
const operation = findLast(
updateProfilerData,
(value) => value.operation === item.dataKey
);
const operationMetricsInfo = currentMetrics.information.map((item) => ({
...item,
stackId,
latestValue: latestOperations.get(item.dataKey)?.rowsAffected,
}));
return {
...item,
stackId: stackId,
latestValue: operation?.timestamp
? customFormatDateTime(operation?.timestamp, DATE_TIME_12_HOUR_FORMAT)
: '--',
};
});
const operationDateMetricsInfo = currentMetrics.information.map((item) => ({
...item,
stackId,
latestValue: latestOperations.get(item.dataKey)?.timestamp
? customFormatDateTime(
latestOperations.get(item.dataKey)?.timestamp,
DATE_TIME_12_HOUR_FORMAT
)
: '--',
}));
return {
operationMetrics: {