Compare commits

...

12 Commits

Author SHA1 Message Date
SagarRajput-7
eb199179b8 fix: code refactor and fix 2025-11-06 09:38:49 +05:30
SagarRajput-7
f2b2f0e550 fix: prevented event bubbling 2025-11-04 10:56:01 +05:30
SagarRajput-7
cdd58e8e20 Merge branch 'main' into SIG-5270 2025-11-04 10:51:17 +05:30
SagarRajput-7
0458b059c3 Merge branch 'main' into SIG-5270 2025-10-21 11:15:15 +05:30
SagarRajput-7
633f719a8b Merge branch 'main' into SIG-5270 2025-10-16 11:49:17 +05:30
SagarRajput-7
2945b10c04 Merge branch 'main' into SIG-5270 2025-10-12 21:05:17 +05:30
SagarRajput-7
763eeeab0b fix: added more checkbox click handler related test cases 2025-10-06 12:13:04 +05:30
SagarRajput-7
d0aaf8fd2f fix: added test cases 2025-10-06 11:54:54 +05:30
SagarRajput-7
894215cb00 Merge branch 'main' into SIG-5270 2025-10-05 21:30:29 +05:30
SagarRajput-7
c5646765fd fix: fixed the click issue and the sync between different clicks label, checkbox and legend 2025-10-05 21:18:50 +05:30
SagarRajput-7
1e47968afe Merge branch 'main' into SIG-5270 2025-10-05 14:11:46 +05:30
SagarRajput-7
dfd4e2e0fe fix: added formating and unit in full view graph manager table 2025-09-30 16:29:06 +05:30
5 changed files with 446 additions and 6 deletions

View File

@@ -54,16 +54,34 @@ function GraphManager({
const labelClickedHandler = useCallback(
(labelIndex: number): void => {
const newGraphVisibilityStates = Array<boolean>(data.length).fill(false);
newGraphVisibilityStates[labelIndex] = true;
if (labelIndex < 0 || labelIndex >= graphsVisibilityStates.length) return;
const newGraphVisibilityStates = [...graphsVisibilityStates];
const isCurrentlyVisible = newGraphVisibilityStates[labelIndex];
const visibleCount = newGraphVisibilityStates.filter(Boolean).length;
if (isCurrentlyVisible && visibleCount === 1) {
newGraphVisibilityStates.fill(true);
} else if (isCurrentlyVisible) {
newGraphVisibilityStates.fill(false);
newGraphVisibilityStates[labelIndex] = true;
} else {
newGraphVisibilityStates[labelIndex] = true;
}
// Update all graphs based on new state
newGraphVisibilityStates.forEach((state, index) => {
lineChartRef?.current?.toggleGraph(index, state);
parentChartRef?.current?.toggleGraph(index, state);
});
setGraphsVisibilityStates(newGraphVisibilityStates);
},
[data.length, lineChartRef, parentChartRef, setGraphsVisibilityStates],
[
graphsVisibilityStates,
lineChartRef,
parentChartRef,
setGraphsVisibilityStates,
],
);
const columns = getGraphManagerTableColumns({

View File

@@ -1,5 +1,6 @@
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ColumnType } from 'antd/es/table';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants';
import { DataSetProps, ExtendedChartDataset } from '../types';
@@ -7,6 +8,20 @@ import { getGraphManagerTableHeaderTitle } from '../utils';
import CustomCheckBox from './CustomCheckBox';
import { getLabel } from './GetLabel';
// Helper function to format numeric values based on yAxisUnit
const formatMetricValue = (
value: number | null | undefined,
yAxisUnit?: string,
): string => {
if (value == null || value === undefined || Number.isNaN(value)) {
return '';
}
if (yAxisUnit) {
return getYAxisFormattedValue(value.toString(), yAxisUnit);
}
return value.toString();
};
export const getGraphManagerTableColumns = ({
tableDataSet,
checkBoxOnChangeHandler,
@@ -45,6 +60,7 @@ export const getGraphManagerTableColumns = ({
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Avg,
key: ColumnsKeyAndDataIndex.Avg,
render: (value: number): string => formatMetricValue(value, yAxisUnit),
},
{
title: getGraphManagerTableHeaderTitle(
@@ -54,6 +70,7 @@ export const getGraphManagerTableColumns = ({
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Sum,
key: ColumnsKeyAndDataIndex.Sum,
render: (value: number): string => formatMetricValue(value, yAxisUnit),
},
{
title: getGraphManagerTableHeaderTitle(
@@ -63,6 +80,7 @@ export const getGraphManagerTableColumns = ({
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Max,
key: ColumnsKeyAndDataIndex.Max,
render: (value: number): string => formatMetricValue(value, yAxisUnit),
},
{
title: getGraphManagerTableHeaderTitle(
@@ -72,10 +90,11 @@ export const getGraphManagerTableColumns = ({
width: 90,
dataIndex: ColumnsKeyAndDataIndex.Min,
key: ColumnsKeyAndDataIndex.Min,
render: (value: number): string => formatMetricValue(value, yAxisUnit),
},
];
interface GetGraphManagerTableColumnsProps {
export interface GetGraphManagerTableColumnsProps {
tableDataSet: ExtendedChartDataset[];
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
labelClickedHandler: (labelIndex: number) => void;

View File

@@ -0,0 +1,338 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable react/jsx-props-no-spreading */
import { render, screen, userEvent } from 'tests/test-utils';
import GraphManager from '../GridCard/FullView/GraphManager';
import {
getGraphManagerTableColumns,
GetGraphManagerTableColumnsProps,
} from '../GridCard/FullView/TableRender/GraphManagerColumns';
import { GraphManagerProps } from '../GridCard/FullView/types';
// Props
const props = {
tableDataSet: [
{
label: 'Timestamp',
stroke: 'purple',
index: 0,
show: true,
sum: 52791867900,
avg: 1759728930,
max: 1759729800,
min: 1759728060,
},
{
drawStyle: 'line',
lineInterpolation: 'spline',
show: true,
label: '{service.name=""}',
stroke: '#B33300',
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: '#B33300',
},
index: 1,
sum: 2274.96,
avg: 75.83,
max: 115.76,
min: 55.64,
},
{
drawStyle: 'line',
lineInterpolation: 'spline',
show: true,
label: '{service.name="recommendationservice"}',
stroke: '#BB6BD9',
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: '#BB6BD9',
},
index: 2,
sum: 1770.84,
avg: 59.028,
max: 112.16,
min: 0,
},
{
drawStyle: 'line',
lineInterpolation: 'spline',
show: true,
label: '{service.name="loadgenerator"}',
stroke: '#E9967A',
width: 2,
spanGaps: true,
points: {
size: 5,
show: false,
stroke: '#E9967A',
},
index: 3,
sum: 1801.25,
avg: 60.041,
max: 94.46,
min: 39.86,
},
],
graphVisibilityState: [true, true, true, true],
yAxisUnit: 'ops',
isGraphDisabled: false,
} as GetGraphManagerTableColumnsProps;
describe('GraphManager', () => {
it('should render the columns', () => {
const columns = getGraphManagerTableColumns({
...props,
});
expect(columns).toStrictEqual([
{
dataIndex: 'index',
key: 'index',
render: expect.any(Function),
title: '',
width: 50,
},
{
dataIndex: 'label',
key: 'label',
render: expect.any(Function),
title: 'Label',
width: 300,
},
{
dataIndex: 'avg',
key: 'avg',
render: expect.any(Function),
title: 'Avg (in ops)',
width: 90,
},
{
dataIndex: 'sum',
key: 'sum',
render: expect.any(Function),
title: 'Sum (in ops)',
width: 90,
},
{
dataIndex: 'max',
key: 'max',
render: expect.any(Function),
title: 'Max (in ops)',
width: 90,
},
{
dataIndex: 'min',
key: 'min',
render: expect.any(Function),
title: 'Min (in ops)',
width: 90,
},
]);
});
it('should render graphmanager with correct formatting using y-axis', () => {
const testProps: GraphManagerProps = {
data: [
[1759729380, 1759729440, 1759729500], // timestamps
[66.167, 76.833, 83.767], // series 1
[46.6, 52.7, 70.867], // series 2
[45.967, 52.967, 69.933], // series 3
],
name: 'test-graph',
yAxisUnit: 'ops',
onToggleModelHandler: jest.fn(),
setGraphsVisibilityStates: jest.fn(),
graphsVisibilityStates: [true, true, true, true],
lineChartRef: { current: { toggleGraph: jest.fn() } },
parentChartRef: { current: { toggleGraph: jest.fn() } },
options: {
series: [
{ label: 'Timestamp' },
{ label: '{service.name=""}' },
{ label: '{service.name="recommendationservice"}' },
{ label: '{service.name="loadgenerator"}' },
],
width: 100,
height: 100,
},
};
// eslint-disable-next-line react/jsx-props-no-spreading
render(<GraphManager {...testProps} />);
// Assert that column headers include y-axis unit formatting
expect(screen.getByText('Avg (in ops)')).toBeInTheDocument();
expect(screen.getByText('Sum (in ops)')).toBeInTheDocument();
expect(screen.getByText('Max (in ops)')).toBeInTheDocument();
expect(screen.getByText('Min (in ops)')).toBeInTheDocument();
// Assert formatting
expect(screen.getByText('75.6 ops/s')).toBeInTheDocument();
expect(screen.getByText('227 ops/s')).toBeInTheDocument();
expect(screen.getByText('83.8 ops/s')).toBeInTheDocument();
expect(screen.getByText('66.2 ops/s')).toBeInTheDocument();
});
it('should handle checkbox click correctly', async () => {
const mockToggleGraph = jest.fn();
const mockSetGraphsVisibilityStates = jest.fn();
const testProps: GraphManagerProps = {
data: [
[1759729380, 1759729440, 1759729500],
[66.167, 76.833, 83.767],
[46.6, 52.7, 70.867],
],
name: 'test-graph',
yAxisUnit: 'ops',
onToggleModelHandler: jest.fn(),
setGraphsVisibilityStates: mockSetGraphsVisibilityStates,
graphsVisibilityStates: [true, true, true],
lineChartRef: { current: { toggleGraph: mockToggleGraph } },
parentChartRef: { current: { toggleGraph: mockToggleGraph } },
options: {
series: [
{ label: 'Timestamp' },
{ label: '{service.name=""}' },
{ label: '{service.name="recommendationservice"}' },
],
width: 100,
height: 100,
},
};
render(<GraphManager {...testProps} />);
// Find the first checkbox input (index 1, since index 0 is timestamp)
const checkbox = screen.getAllByRole('checkbox')[0];
expect(checkbox).toBeInTheDocument();
// Simulate checkbox click
await userEvent.click(checkbox);
// Verify toggleGraph was called on both chart refs
expect(mockToggleGraph).toHaveBeenCalledWith(1, false);
expect(mockToggleGraph).toHaveBeenCalledTimes(2); // lineChartRef and parentChartRef
// Verify state update function was called
expect(mockSetGraphsVisibilityStates).toHaveBeenCalledWith([
true,
false,
true,
]);
});
it('should handle label click correctly for visibility toggle', async () => {
const mockToggleGraph = jest.fn();
const mockSetGraphsVisibilityStates = jest.fn();
const testProps: GraphManagerProps = {
data: [
[1759729380, 1759729440, 1759729500],
[66.167, 76.833, 83.767],
[46.6, 52.7, 70.867],
],
name: 'test-graph',
yAxisUnit: 'ops',
onToggleModelHandler: jest.fn(),
setGraphsVisibilityStates: mockSetGraphsVisibilityStates,
graphsVisibilityStates: [true, true, true],
lineChartRef: { current: { toggleGraph: mockToggleGraph } },
parentChartRef: { current: { toggleGraph: mockToggleGraph } },
options: {
series: [
{ label: 'Timestamp' },
{ label: '{service.name="loadgenerator"}' },
{ label: '{service.name="recommendationservice"}' },
],
width: 100,
height: 100,
},
};
render(<GraphManager {...testProps} />);
// Find the first label button (skip Cancel and Save buttons)
const buttons = screen.getAllByRole('button');
const label = buttons.find((button) =>
button.textContent?.includes('{service.name="loadgenerator"}'),
) as HTMLElement;
expect(label).toBeInTheDocument();
// Simulate label click
await userEvent.click(label);
// Verify setGraphsVisibilityStates was called with show-only behavior
expect(mockSetGraphsVisibilityStates).toHaveBeenCalledWith([
false,
true,
false,
]);
// Check if toggleGraph was called for each series
expect(mockToggleGraph).toHaveBeenCalledWith(0, false); // timestamp
expect(mockToggleGraph).toHaveBeenCalledWith(1, true); // selected series
expect(mockToggleGraph).toHaveBeenCalledWith(2, false); // other series
expect(mockToggleGraph).toHaveBeenCalledTimes(6); // 3 series × 2 chart refs
});
it('should handle label click to show all when only one is visible', async () => {
const mockToggleGraph = jest.fn();
const mockSetGraphsVisibilityStates = jest.fn();
const testProps: GraphManagerProps = {
data: [
[1759729380, 1759729440, 1759729500],
[66.167, 76.833, 83.767],
[46.6, 52.7, 70.867],
],
name: 'test-graph',
yAxisUnit: 'ops',
onToggleModelHandler: jest.fn(),
setGraphsVisibilityStates: mockSetGraphsVisibilityStates,
graphsVisibilityStates: [false, true, false], // Only one series visible
lineChartRef: { current: { toggleGraph: mockToggleGraph } },
parentChartRef: { current: { toggleGraph: mockToggleGraph } },
options: {
series: [
{ label: 'Timestamp' },
{ label: '{service.name=""}' },
{ label: '{service.name="recommendationservice"}' },
],
width: 100,
height: 100,
},
};
render(<GraphManager {...testProps} />);
// Find the visible label button (skip Cancel and Save buttons)
const buttons = screen.getAllByRole('button');
const label = buttons.find((button) =>
button.textContent?.includes('{service.name=""}'),
) as HTMLElement;
expect(label).toBeInTheDocument();
// Simulate label click (should show all since only this one is visible)
await userEvent.click(label);
// Verify setGraphsVisibilityStates was called with show-all behavior
expect(mockSetGraphsVisibilityStates).toHaveBeenCalledWith([
true,
true,
true,
]);
// Check if toggleGraph was called to show all series
expect(mockToggleGraph).toHaveBeenCalledWith(0, true); // timestamp
expect(mockToggleGraph).toHaveBeenCalledWith(1, true); // current series
expect(mockToggleGraph).toHaveBeenCalledWith(2, true); // other series
expect(mockToggleGraph).toHaveBeenCalledTimes(6); // 3 series × 2 chart refs
});
});

View File

@@ -700,7 +700,27 @@ export const getUPlotChartOptions = ({
}
};
currentMarker.addEventListener('click', markerClickHandler);
requestAnimationFrame(() => {
const currentMarkerElement = thElement.querySelector(
'.u-marker',
) as HTMLElement;
if (currentMarkerElement) {
currentMarkerElement.classList.add('u-marker-clickable');
currentMarkerElement.addEventListener(
'click',
markerClickHandler,
false,
);
currentMarkerElement.addEventListener(
'mousedown',
(e) => {
e.preventDefault();
markerClickHandler(e);
},
false,
);
}
});
// Store cleanup function for marker click listener
(self as ExtendedUPlot)._legendElementCleanup?.push(() => {
@@ -710,6 +730,7 @@ export const getUPlotChartOptions = ({
// Text click handler - show only/show all behavior (existing behavior)
if (textElement) {
// Create the click handler function
const textClickHandler = (e: Event): void => {
e.stopPropagation?.(); // Prevent event bubbling
@@ -743,7 +764,45 @@ export const getUPlotChartOptions = ({
}
};
textElement.addEventListener('click', textClickHandler);
// Use requestAnimationFrame to ensure DOM is fully ready
requestAnimationFrame(() => {
// Re-query the element to ensure we have the current DOM element
const currentTextElement = thElement.querySelector(
'.legend-text',
) as HTMLElement;
if (currentTextElement) {
// Force the element to be clickable
currentTextElement.style.cursor = 'pointer';
currentTextElement.style.pointerEvents = 'auto';
// Add multiple event listeners to ensure we catch the click
currentTextElement.addEventListener(
'click',
textClickHandler,
false,
);
currentTextElement.addEventListener(
'mousedown',
(e) => {
e.preventDefault();
textClickHandler(e);
},
false,
);
// Also add to the parent th element as a fallback
thElement.addEventListener(
'click',
(e) => {
if (e.target === currentTextElement) {
textClickHandler();
}
},
false,
);
}
});
// Store cleanup function for text click listener
(self as ExtendedUPlot)._legendElementCleanup?.push(() => {

View File

@@ -74,6 +74,12 @@ body {
.u-marker {
border-radius: 50%;
// Clickable marker styles
&.u-marker-clickable {
cursor: pointer;
pointer-events: auto;
}
}
}