mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-05 03:54:23 +00:00
* 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:
parent
0a3c938b67
commit
f9df0793d6
@ -41,6 +41,7 @@ export interface ProfilerDetailsCardProps {
|
||||
curveType?: CurveType;
|
||||
isLoading?: boolean;
|
||||
noDataPlaceholderText?: ReactNode;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export enum TableProfilerTab {
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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>
|
||||
) : (
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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>
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -64,6 +64,7 @@ export const PROFILER_METRIC = [
|
||||
'histogram',
|
||||
'customMetricsProfile',
|
||||
];
|
||||
export const PROFILER_CHART_DATA_SIZE = 500;
|
||||
|
||||
export const PROFILER_FILTER_RANGE: DateFilterType = {
|
||||
yesterday: {
|
||||
|
||||
@ -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>
|
||||
)),
|
||||
}));
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user