mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-30 03:00:59 +00:00
Compare commits
12 Commits
update-PR-
...
SIG-5270
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb199179b8 | ||
|
|
f2b2f0e550 | ||
|
|
cdd58e8e20 | ||
|
|
0458b059c3 | ||
|
|
633f719a8b | ||
|
|
2945b10c04 | ||
|
|
763eeeab0b | ||
|
|
d0aaf8fd2f | ||
|
|
894215cb00 | ||
|
|
c5646765fd | ||
|
|
1e47968afe | ||
|
|
dfd4e2e0fe |
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -74,6 +74,12 @@ body {
|
||||
|
||||
.u-marker {
|
||||
border-radius: 50%;
|
||||
|
||||
// Clickable marker styles
|
||||
&.u-marker-clickable {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user