mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-25 17:52:23 +00:00
Compare commits
4 Commits
feat/cloud
...
fix/memory
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0d7ed56e3 | ||
|
|
4f0eff50af | ||
|
|
2102ab6df3 | ||
|
|
fefa60bfd3 |
@@ -0,0 +1,131 @@
|
||||
import { ColorPickerProps } from 'antd';
|
||||
import { Color } from 'antd/es/color-picker';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import LegendColors from './LegendColors';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
__esModule: true,
|
||||
useQueryBuilder: (): { currentQuery: unknown } => ({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'A',
|
||||
legend: '{service.name}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('antd', () => {
|
||||
const actual = jest.requireActual('antd');
|
||||
return {
|
||||
...actual,
|
||||
ColorPicker: ({ onChange, children }: ColorPickerProps): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="legend-color-picker"
|
||||
onClick={(): void =>
|
||||
onChange!({ toHexString: (): string => '#ffffff' } as Color, '#ffffff')
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('LegendColors', () => {
|
||||
it('renders legend colors panel and items', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<LegendColors
|
||||
customLegendColors={{}}
|
||||
setCustomLegendColors={jest.fn()}
|
||||
queryResponse={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Legend Colors')).toBeInTheDocument();
|
||||
|
||||
// Expand the collapse to reveal legend items
|
||||
await user.click(
|
||||
screen.getByRole('tab', {
|
||||
name: /Legend Colors/i,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(screen.getByText('{service.name}')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls setCustomLegendColors when color is changed', async () => {
|
||||
const user = userEvent.setup();
|
||||
const setCustomLegendColors = jest.fn();
|
||||
|
||||
render(
|
||||
<LegendColors
|
||||
customLegendColors={{}}
|
||||
setCustomLegendColors={setCustomLegendColors}
|
||||
queryResponse={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Expand to render the mocked ColorPicker button
|
||||
await user.click(
|
||||
screen.getByRole('tab', {
|
||||
name: /Legend Colors/i,
|
||||
}),
|
||||
);
|
||||
|
||||
const colorTrigger = screen.getByTestId('legend-color-picker');
|
||||
|
||||
await user.click(colorTrigger);
|
||||
|
||||
expect(setCustomLegendColors).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throttles rapid color changes', async () => {
|
||||
jest.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||
const setCustomLegendColors = jest.fn();
|
||||
|
||||
render(
|
||||
<LegendColors
|
||||
customLegendColors={{}}
|
||||
setCustomLegendColors={setCustomLegendColors}
|
||||
queryResponse={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Expand panel to render the mocked ColorPicker button
|
||||
await user.click(
|
||||
screen.getByRole('tab', {
|
||||
name: /Legend Colors/i,
|
||||
}),
|
||||
);
|
||||
|
||||
const colorTrigger = screen.getByTestId('legend-color-picker');
|
||||
|
||||
// Fire multiple rapid changes
|
||||
await user.click(colorTrigger);
|
||||
await user.click(colorTrigger);
|
||||
await user.click(colorTrigger);
|
||||
await user.click(colorTrigger);
|
||||
|
||||
// Flush pending throttled calls
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
// Throttling should ensure we don't invoke the setter once per click
|
||||
expect(setCustomLegendColors).toHaveBeenCalledTimes(2);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import { Palette } from 'lucide-react';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
@@ -95,13 +96,24 @@ function LegendColors({
|
||||
);
|
||||
};
|
||||
|
||||
// Handle color change
|
||||
const handleColorChange = (label: string, color: string): void => {
|
||||
setCustomLegendColors((prev) => ({
|
||||
...prev,
|
||||
[label]: color,
|
||||
}));
|
||||
};
|
||||
// Handle color change (throttled to avoid excessive updates)
|
||||
const handleColorChange = useMemo(
|
||||
() =>
|
||||
throttle((label: string, color: string): void => {
|
||||
setCustomLegendColors((prev) => ({
|
||||
...prev,
|
||||
[label]: color,
|
||||
}));
|
||||
}, 200), // 200ms is a good compromise between responsiveness and performance
|
||||
[setCustomLegendColors],
|
||||
);
|
||||
|
||||
// Clean up throttled handler on unmount
|
||||
useEffect(() => {
|
||||
return (): void => {
|
||||
handleColorChange.cancel();
|
||||
};
|
||||
}, [handleColorChange]);
|
||||
|
||||
// Reset to default color
|
||||
const resetToDefault = (label: string): void => {
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
.tooltip-plugin-container {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 1070;
|
||||
white-space: pre;
|
||||
border-radius: 4px;
|
||||
position: fixed;
|
||||
overflow: auto;
|
||||
transform: translate(-1000px, -1000px); // hide the tooltip initially
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&.pinned {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,8 +339,9 @@ export default function TooltipPlugin({
|
||||
return;
|
||||
}
|
||||
const layout = layoutRef.current;
|
||||
layout.observer.disconnect();
|
||||
|
||||
if (containerRef.current) {
|
||||
layout.observer.disconnect();
|
||||
layout.observer.observe(containerRef.current);
|
||||
const { width, height } = containerRef.current.getBoundingClientRect();
|
||||
layout.width = width;
|
||||
@@ -351,24 +352,28 @@ export default function TooltipPlugin({
|
||||
}
|
||||
}, [isHovering, plot]);
|
||||
|
||||
if (!plot || !isHovering) {
|
||||
if (!plot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={cx('tooltip-plugin-container', { pinned: isPinned })}
|
||||
className={cx('tooltip-plugin-container', {
|
||||
pinned: isPinned,
|
||||
visible: isHovering,
|
||||
})}
|
||||
style={{
|
||||
...style,
|
||||
maxWidth: `${maxWidth}px`,
|
||||
maxHeight: `${maxHeight}px`,
|
||||
width: '100%',
|
||||
}}
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
aria-hidden={!isHovering}
|
||||
ref={containerRef}
|
||||
data-testid="tooltip-plugin-container"
|
||||
>
|
||||
{contents}
|
||||
{isHovering ? contents : null}
|
||||
</div>,
|
||||
portalRoot.current,
|
||||
);
|
||||
|
||||
@@ -187,9 +187,7 @@ describe('TooltipPlugin', () => {
|
||||
canPinTooltip: true,
|
||||
});
|
||||
|
||||
const container = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement;
|
||||
const container = screen.getByTestId('tooltip-plugin-container');
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
|
||||
act(() => {
|
||||
@@ -197,11 +195,9 @@ describe('TooltipPlugin', () => {
|
||||
});
|
||||
|
||||
return waitFor(() => {
|
||||
const updated = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement | null;
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated?.classList.contains('pinned')).toBe(true);
|
||||
const updated = screen.getByTestId('tooltip-plugin-container');
|
||||
expect(updated).toBeInTheDocument();
|
||||
expect(updated.classList.contains('pinned')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -249,7 +245,13 @@ describe('TooltipPlugin', () => {
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
const container = screen.getByTestId('tooltip-plugin-container');
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container.getAttribute('aria-hidden')).toBe('true');
|
||||
expect(container.classList.contains('visible')).toBe(false);
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
expect(container.textContent).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -292,12 +294,16 @@ describe('TooltipPlugin', () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
const container = screen.getByTestId('tooltip-plugin-container');
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container.getAttribute('aria-hidden')).toBe('true');
|
||||
expect(container.classList.contains('visible')).toBe(false);
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('unpins the tooltip on outside mousedown', () => {
|
||||
it('unpins the tooltip on outside mousedown', async () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
@@ -335,12 +341,19 @@ describe('TooltipPlugin', () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
await waitFor(() => {
|
||||
const container = screen.getByTestId('tooltip-plugin-container');
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container.getAttribute('aria-hidden')).toBe('true');
|
||||
expect(container.classList.contains('visible')).toBe(false);
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('unpins the tooltip on outside keydown', () => {
|
||||
it('unpins the tooltip on outside keydown', async () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
@@ -380,7 +393,13 @@ describe('TooltipPlugin', () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
await waitFor(() => {
|
||||
const container = screen.getByTestId('tooltip-plugin-container');
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container.getAttribute('aria-hidden')).toBe('true');
|
||||
expect(container.classList.contains('visible')).toBe(false);
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user