diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface.ts index 4f1d20884f7..bd11cd1479e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface.ts @@ -41,6 +41,7 @@ export interface ProfilerDetailsCardProps { curveType?: CurveType; isLoading?: boolean; noDataPlaceholderText?: ReactNode; + children?: ReactNode; } export enum TableProfilerTab { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDetailsCard/ProfilerDetailsCard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDetailsCard/ProfilerDetailsCard.test.tsx index f02ee55e8d3..4af75975414 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDetailsCard/ProfilerDetailsCard.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDetailsCard/ProfilerDetailsCard.test.tsx @@ -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(() =>
CustomTooltip
), })); +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(); @@ -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( + + ); + + expect(screen.getByText('Brush')).toBeInTheDocument(); }); it('No data should be rendered', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDetailsCard/ProfilerDetailsCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDetailsCard/ProfilerDetailsCard.tsx index 87af9db26a2..cf47cc5ee48 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDetailsCard/ProfilerDetailsCard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDetailsCard/ProfilerDetailsCard.tsx @@ -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) => { const { data, information } = chartCollection; const [activeKeys, setActiveKeys] = useState([]); + 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 = ({ /> ))} + {showBrush && ( + + )} ) : ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerStateWrapper/ProfilerStateWrapper.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerStateWrapper/ProfilerStateWrapper.component.tsx new file mode 100644 index 00000000000..ce25e9ca7bb --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerStateWrapper/ProfilerStateWrapper.component.tsx @@ -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 ( + + + + {title} + + + + + {children} + + + ); +}; + +export default ProfilerStateWrapper; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerStateWrapper/ProfilerStateWrapper.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerStateWrapper/ProfilerStateWrapper.interface.ts new file mode 100644 index 00000000000..0dec155592f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerStateWrapper/ProfilerStateWrapper.interface.ts @@ -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; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerStateWrapper/ProfilerStateWrapper.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerStateWrapper/ProfilerStateWrapper.test.tsx new file mode 100644 index 00000000000..ca60aae479d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerStateWrapper/ProfilerStateWrapper.test.tsx @@ -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(() => ( +
ProfilerLatestValue
+ )); +}); + +describe('ProfilerStateWrapper', () => { + const mockProfilerLatestValueProps = { + information: [ + { + title: 'Test Label', + dataKey: 'testKey', + color: '#000000', + latestValue: '100', + }, + ], + }; + + const defaultProps = { + isLoading: false, + title: 'Test Title', + profilerLatestValueProps: mockProfilerLatestValueProps, + children:
Test Content
, + }; + + it('renders without crashing', () => { + render(); + + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByText('Test Content')).toBeInTheDocument(); + }); + + it('shows loading state when isLoading is true', () => { + render(); + + 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( + + ); + + expect(screen.getByTestId(customTestId)).toBeInTheDocument(); + }); + + it('renders with default data-testid when not provided', () => { + render(); + + expect( + screen.getByTestId('profiler-details-card-container') + ).toBeInTheDocument(); + }); + + it('renders ProfilerLatestValue with correct props', () => { + render(); + + const profilerLatestValue = screen.getByTestId('profiler-latest-value'); + + expect(profilerLatestValue).toBeInTheDocument(); + }); + + it('renders children content correctly', () => { + const { title, profilerLatestValueProps, isLoading } = defaultProps; + + render( + +
+

Custom Header

+

Custom Paragraph

+
+
+ ); + + expect(screen.getByText('Custom Header')).toBeInTheDocument(); + expect(screen.getByText('Custom Paragraph')).toBeInTheDocument(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/TableProfilerChart/TableProfilerChart.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/TableProfilerChart/TableProfilerChart.tsx index eb59c5a754f..7c840f9f7ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/TableProfilerChart/TableProfilerChart.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/TableProfilerChart/TableProfilerChart.tsx @@ -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(INITIAL_OPERATION_METRIC_VALUE); - const [isLoading, setIsLoading] = useState(true); + const [isTableProfilerLoading, setIsTableProfilerLoading] = useState(true); + const [isSystemProfilerLoading, setIsSystemProfilerLoading] = useState(true); const [profileMetrics, setProfileMetrics] = useState([]); 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 ( + + + + ); + }, [isSystemProfilerLoading, operationDateMetrics, noProfilerMessage]); + + const operationMetricsCard = useMemo(() => { + return ( + + + + ); + }, [isSystemProfilerLoading, operationMetrics, noProfilerMessage]); return ( @@ -290,68 +324,19 @@ const TableProfilerChart = ({ - - - - - - {t('label.table-update-plural')} - - - - - - - - - - - - - - - - - {t('label.volume-change')} - - - - - - - - - - - + {operationDateMetricsCard} + {operationMetricsCard} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/CustomBarChart.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/CustomBarChart.test.tsx index c9cb42490e6..59ca49cff81 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/CustomBarChart.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/CustomBarChart.test.tsx @@ -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(
ErrorPlaceHolder
), })); +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( + + ); + + expect(screen.getByText('Brush')).toBeInTheDocument(); }); it('If there is no data, placeholder should be visible', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/CustomBarChart.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/CustomBarChart.tsx index 59574b9339a..24829887f09 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/CustomBarChart.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/CustomBarChart.tsx @@ -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([]); + const { showBrush, endIndex } = useMemo(() => { + return { + showBrush: data.length > PROFILER_CHART_DATA_SIZE, + endIndex: PROFILER_CHART_DATA_SIZE, + }; + }, [data.length]); + if (data.length === 0) { return ( @@ -104,6 +113,15 @@ const CustomBarChart = ({ /> ))} + {showBrush && ( + + )} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/OperationDateBarChart.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/OperationDateBarChart.test.tsx index ed6f90aa030..91b08b456ae 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/OperationDateBarChart.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/OperationDateBarChart.test.tsx @@ -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( + + ); + + expect(screen.getByText('Brush')).toBeInTheDocument(); }); it('If there is no data, placeholder should be visible', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/OperationDateBarChart.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/OperationDateBarChart.tsx index 084287da84f..e8ad7985197 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/OperationDateBarChart.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Visualisations/Chart/OperationDateBarChart.tsx @@ -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([]); + 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 }} /> + ''} + tickLine={false} + /> {information.map((info) => ( - - ))} - {information.map((info) => ( - + + + + ))} + {showBrush && ( + + )} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/profiler.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/profiler.constant.ts index bbc852d5030..09e49736760 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/profiler.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/profiler.constant.ts @@ -64,6 +64,7 @@ export const PROFILER_METRIC = [ 'histogram', 'customMetricsProfile', ]; +export const PROFILER_CHART_DATA_SIZE = 500; export const PROFILER_FILTER_RANGE: DateFilterType = { yesterday: { diff --git a/openmetadata-ui/src/main/resources/ui/src/test/unit/mocks/recharts.mock.js b/openmetadata-ui/src/main/resources/ui/src/test/unit/mocks/recharts.mock.js index 7156fe87ee6..e98255ec40a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/test/unit/mocks/recharts.mock.js +++ b/openmetadata-ui/src/main/resources/ui/src/test/unit/mocks/recharts.mock.js @@ -14,6 +14,8 @@ import React from 'react'; jest.mock('recharts', () => ({ Bar: jest.fn().mockImplementation(() =>
Bar
), + Line: jest.fn().mockImplementation(() =>
Line
), + Brush: jest.fn().mockImplementation(() =>
Brush
), Area: jest.fn().mockImplementation(() =>
Area
), Scatter: jest.fn().mockImplementation(() =>
Scatter
), CartesianGrid: jest.fn().mockImplementation(() =>
CartesianGrid
), @@ -27,6 +29,9 @@ jest.mock('recharts', () => ({ AreaChart: jest .fn() .mockImplementation(({ children }) =>
{children}
), + LineChart: jest + .fn() + .mockImplementation(({ children }) =>
{children}
), ComposedChart: jest .fn() .mockImplementation(({ children }) =>
{children}
), @@ -35,9 +40,9 @@ jest.mock('recharts', () => ({ .mockImplementation(({ children, ...rest }) => (
{children}
)), - ResponsiveContainer: jest - .fn() - .mockImplementation(({ children }) => ( -
{children}
- )), + ResponsiveContainer: jest.fn().mockImplementation(({ children, ...rest }) => ( +
+ {children} +
+ )), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableProfilerUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TableProfilerUtils.ts index b02b18c09ee..0353d0c86d0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableProfilerUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableProfilerUtils.ts @@ -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(); - 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: {