mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-10 19:52:06 +00:00
Compare commits
3 Commits
ns/ip-filt
...
test/toolt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6156156a85 | ||
|
|
fb0d2830ae | ||
|
|
04e5d87986 |
@@ -103,7 +103,7 @@ export const getTotalLogSizeWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.SUM,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -140,7 +140,7 @@ export const getTotalTraceSizeWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.SUM,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -177,7 +177,7 @@ export const getTotalMetricDatapointCountWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.SUM,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -214,7 +214,7 @@ export const getLogCountWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -251,7 +251,7 @@ export const getLogSizeWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -288,7 +288,7 @@ export const getSpanCountWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -325,7 +325,7 @@ export const getSpanSizeWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
@@ -362,7 +362,7 @@ export const getMetricCountWidgetData = (): Widgets =>
|
||||
queryName: 'A',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
spaceAggregation: 'sum',
|
||||
stepInterval: null,
|
||||
stepInterval: 60,
|
||||
timeAggregation: 'increase',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { FilterOutlined, VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { Atom, Binoculars, SquareMousePointer, Terminal } from 'lucide-react';
|
||||
@@ -32,7 +32,6 @@ export default function LeftToolbarActions({
|
||||
<Tooltip title="Show Filters">
|
||||
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
|
||||
<FilterOutlined />
|
||||
<VerticalAlignTopOutlined rotate={90} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
width: 40px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 12px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
|
||||
@@ -95,6 +95,7 @@ function ResourceAttributesFilter({
|
||||
data-testid="resource-environment-filter"
|
||||
style={{ minWidth: 200, height: 34 }}
|
||||
onChange={handleEnvironmentChange}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
{environments.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value}>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
import { whilelistedKeys } from '../config';
|
||||
import { mappingWithRoutesAndKeys } from '../utils';
|
||||
|
||||
describe('useResourceAttribute config', () => {
|
||||
describe('whilelistedKeys', () => {
|
||||
it('should include underscore-notation keys (DOT_METRICS_ENABLED=false)', () => {
|
||||
expect(whilelistedKeys).toContain('resource_deployment_environment');
|
||||
expect(whilelistedKeys).toContain('resource_k8s_cluster_name');
|
||||
expect(whilelistedKeys).toContain('resource_k8s_cluster_namespace');
|
||||
});
|
||||
|
||||
it('should include dot-notation keys (DOT_METRICS_ENABLED=true)', () => {
|
||||
expect(whilelistedKeys).toContain('resource_deployment.environment');
|
||||
expect(whilelistedKeys).toContain('resource_k8s.cluster.name');
|
||||
expect(whilelistedKeys).toContain('resource_k8s.cluster.namespace');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mappingWithRoutesAndKeys', () => {
|
||||
const dotNotationFilters = [
|
||||
{
|
||||
label: 'deployment.environment',
|
||||
value: 'resource_deployment.environment',
|
||||
},
|
||||
{ label: 'k8s.cluster.name', value: 'resource_k8s.cluster.name' },
|
||||
{ label: 'k8s.cluster.namespace', value: 'resource_k8s.cluster.namespace' },
|
||||
];
|
||||
|
||||
const underscoreNotationFilters = [
|
||||
{
|
||||
label: 'deployment.environment',
|
||||
value: 'resource_deployment_environment',
|
||||
},
|
||||
{ label: 'k8s.cluster.name', value: 'resource_k8s_cluster_name' },
|
||||
{ label: 'k8s.cluster.namespace', value: 'resource_k8s_cluster_namespace' },
|
||||
];
|
||||
|
||||
const nonWhitelistedFilters = [
|
||||
{ label: 'host.name', value: 'resource_host_name' },
|
||||
{ label: 'service.name', value: 'resource_service_name' },
|
||||
];
|
||||
|
||||
it('should keep dot-notation filters on the Service Map route', () => {
|
||||
const result = mappingWithRoutesAndKeys(
|
||||
ROUTES.SERVICE_MAP,
|
||||
dotNotationFilters,
|
||||
);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toEqual(dotNotationFilters);
|
||||
});
|
||||
|
||||
it('should keep underscore-notation filters on the Service Map route', () => {
|
||||
const result = mappingWithRoutesAndKeys(
|
||||
ROUTES.SERVICE_MAP,
|
||||
underscoreNotationFilters,
|
||||
);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toEqual(underscoreNotationFilters);
|
||||
});
|
||||
|
||||
it('should filter out non-whitelisted keys on the Service Map route', () => {
|
||||
const allFilters = [...dotNotationFilters, ...nonWhitelistedFilters];
|
||||
const result = mappingWithRoutesAndKeys(ROUTES.SERVICE_MAP, allFilters);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toEqual(dotNotationFilters);
|
||||
});
|
||||
|
||||
it('should return all filters on non-Service Map routes', () => {
|
||||
const allFilters = [...dotNotationFilters, ...nonWhitelistedFilters];
|
||||
const result = mappingWithRoutesAndKeys('/services', allFilters);
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result).toEqual(allFilters);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,5 @@
|
||||
export const whilelistedKeys = [
|
||||
'resource_deployment_environment',
|
||||
'resource_deployment.environment',
|
||||
'resource_k8s_cluster_name',
|
||||
'resource_k8s.cluster.name',
|
||||
'resource_k8s_cluster_namespace',
|
||||
'resource_k8s.cluster.namespace',
|
||||
];
|
||||
|
||||
477
frontend/src/lib/uPlotV2/plugins/__tests__/TooltipPlugin.test.ts
Normal file
477
frontend/src/lib/uPlotV2/plugins/__tests__/TooltipPlugin.test.ts
Normal file
@@ -0,0 +1,477 @@
|
||||
import React from 'react';
|
||||
import { act, screen, waitFor } from '@testing-library/react';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import TooltipPlugin from '../TooltipPlugin/TooltipPlugin';
|
||||
import { DashboardCursorSync } from '../TooltipPlugin/types';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type HookHandler = (...args: any[]) => void;
|
||||
|
||||
interface ConfigMock {
|
||||
scales: Array<{ props: { time?: boolean } }>;
|
||||
setCursor: jest.Mock;
|
||||
addHook: jest.Mock<() => void, [string, HookHandler]> & {
|
||||
removeCallbacks: jest.Mock[];
|
||||
};
|
||||
}
|
||||
|
||||
function createConfigMock(
|
||||
overrides: Partial<ConfigMock> = {},
|
||||
): ConfigMock & { removeCallbacks: jest.Mock[] } {
|
||||
const removeCallbacks: jest.Mock[] = [];
|
||||
|
||||
const addHook = Object.assign(
|
||||
jest.fn((_: string, _handler: HookHandler) => {
|
||||
const remove = jest.fn();
|
||||
removeCallbacks.push(remove);
|
||||
return remove;
|
||||
}),
|
||||
{ removeCallbacks },
|
||||
) as ConfigMock['addHook'];
|
||||
|
||||
return {
|
||||
scales: [{ props: { time: false } }],
|
||||
setCursor: jest.fn(),
|
||||
addHook,
|
||||
...overrides,
|
||||
removeCallbacks,
|
||||
};
|
||||
}
|
||||
|
||||
function getHandler(config: ConfigMock, hookName: string): HookHandler {
|
||||
const call = config.addHook.mock.calls.find(([name]) => name === hookName);
|
||||
if (!call) {
|
||||
throw new Error(`Hook "${hookName}" was not registered on config`);
|
||||
}
|
||||
return call[1];
|
||||
}
|
||||
|
||||
function createFakePlot(): {
|
||||
over: HTMLDivElement;
|
||||
setCursor: jest.Mock;
|
||||
cursor: { event: Record<string, unknown> };
|
||||
} {
|
||||
return {
|
||||
over: document.createElement('div'),
|
||||
setCursor: jest.fn(),
|
||||
cursor: { event: {} },
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('TooltipPlugin', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
|
||||
(callback as FrameRequestCallback)(0);
|
||||
return 0;
|
||||
});
|
||||
jest
|
||||
.spyOn(window, 'cancelAnimationFrame')
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
/**
|
||||
* Shorthand: render the plugin, initialise a fake plot, and trigger a
|
||||
* series focus so the tooltip becomes visible. Returns the fake plot
|
||||
* instance for further interaction (e.g. clicking the overlay).
|
||||
*/
|
||||
function renderAndActivateHover(
|
||||
config: ConfigMock,
|
||||
renderFn: (...args: any[]) => React.ReactNode = (): React.ReactNode =>
|
||||
React.createElement('div', null, 'tooltip-body'),
|
||||
extraProps: Record<string, unknown> = {},
|
||||
): ReturnType<typeof createFakePlot> {
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: renderFn,
|
||||
syncMode: DashboardCursorSync.None,
|
||||
...extraProps,
|
||||
}),
|
||||
);
|
||||
|
||||
const fakePlot = createFakePlot();
|
||||
const initHandler = getHandler(config, 'init');
|
||||
const setSeriesHandler = getHandler(config, 'setSeries');
|
||||
|
||||
act(() => {
|
||||
initHandler(fakePlot);
|
||||
setSeriesHandler(fakePlot, 1, { focus: true });
|
||||
});
|
||||
|
||||
return fakePlot;
|
||||
}
|
||||
|
||||
// ---- Initial state --------------------------------------------------------
|
||||
|
||||
describe('before any interaction', () => {
|
||||
it('does not render anything when there is no active hover', () => {
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: () => React.createElement('div', null, 'tooltip-body'),
|
||||
syncMode: DashboardCursorSync.None,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
});
|
||||
|
||||
it('registers all required uPlot hooks on mount', () => {
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: () => null,
|
||||
syncMode: DashboardCursorSync.None,
|
||||
}),
|
||||
);
|
||||
|
||||
const registered = config.addHook.mock.calls.map(([name]) => name);
|
||||
expect(registered).toContain('ready');
|
||||
expect(registered).toContain('init');
|
||||
expect(registered).toContain('setData');
|
||||
expect(registered).toContain('setSeries');
|
||||
expect(registered).toContain('setLegend');
|
||||
expect(registered).toContain('setCursor');
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Tooltip rendering ------------------------------------------------------
|
||||
|
||||
describe('tooltip rendering', () => {
|
||||
it('renders contents into a portal on document.body when hover is active', () => {
|
||||
const config = createConfigMock();
|
||||
const renderTooltip = jest.fn(() =>
|
||||
React.createElement('div', null, 'tooltip-body'),
|
||||
);
|
||||
|
||||
renderAndActivateHover(config, renderTooltip);
|
||||
|
||||
expect(renderTooltip).toHaveBeenCalled();
|
||||
expect(screen.getByText('tooltip-body')).toBeInTheDocument();
|
||||
|
||||
const container = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement;
|
||||
expect(container).not.toBeNull();
|
||||
expect(container.parentElement).toBe(document.body);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Pin behaviour ----------------------------------------------------------
|
||||
|
||||
describe('pin behaviour', () => {
|
||||
it('pins the tooltip when canPinTooltip is true and overlay is clicked', () => {
|
||||
const config = createConfigMock();
|
||||
|
||||
const fakePlot = renderAndActivateHover(config, undefined, {
|
||||
canPinTooltip: true,
|
||||
});
|
||||
|
||||
const container = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement;
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
|
||||
act(() => {
|
||||
fakePlot.over.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
return waitFor(() => {
|
||||
const updated = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement | null;
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated?.classList.contains('pinned')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('dismisses a pinned tooltip via the dismiss callback', async () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: (args: any) =>
|
||||
React.createElement(
|
||||
'button',
|
||||
{ type: 'button', onClick: args.dismiss },
|
||||
'Dismiss',
|
||||
),
|
||||
syncMode: DashboardCursorSync.None,
|
||||
canPinTooltip: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const fakePlot = createFakePlot();
|
||||
|
||||
act(() => {
|
||||
getHandler(config, 'init')(fakePlot);
|
||||
getHandler(config, 'setSeries')(fakePlot, 1, { focus: true });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
// Pin the tooltip.
|
||||
act(() => {
|
||||
fakePlot.over.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
const button = await screen.findByRole('button', { name: 'Dismiss' });
|
||||
|
||||
act(() => {
|
||||
button.click();
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('drops a pinned tooltip when the underlying data changes', () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: () => React.createElement('div', null, 'tooltip-body'),
|
||||
syncMode: DashboardCursorSync.None,
|
||||
canPinTooltip: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const fakePlot = createFakePlot();
|
||||
|
||||
act(() => {
|
||||
getHandler(config, 'init')(fakePlot);
|
||||
getHandler(config, 'setSeries')(fakePlot, 1, { focus: true });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
// Pin.
|
||||
act(() => {
|
||||
fakePlot.over.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(
|
||||
(document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement)?.classList.contains('pinned'),
|
||||
).toBe(true);
|
||||
|
||||
// Simulate data update – should dismiss the pinned tooltip.
|
||||
act(() => {
|
||||
getHandler(config, 'setData')(fakePlot);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('unpins the tooltip on outside mousedown', () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: () => React.createElement('div', null, 'pinned content'),
|
||||
syncMode: DashboardCursorSync.None,
|
||||
canPinTooltip: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const fakePlot = createFakePlot();
|
||||
|
||||
act(() => {
|
||||
getHandler(config, 'init')(fakePlot);
|
||||
getHandler(config, 'setSeries')(fakePlot, 1, { focus: true });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fakePlot.over.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(
|
||||
document
|
||||
.querySelector('.tooltip-plugin-container')
|
||||
?.classList.contains('pinned'),
|
||||
).toBe(true);
|
||||
|
||||
// Click outside the tooltip container.
|
||||
act(() => {
|
||||
document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('unpins the tooltip on outside keydown', () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: () => React.createElement('div', null, 'pinned content'),
|
||||
syncMode: DashboardCursorSync.None,
|
||||
canPinTooltip: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const fakePlot = createFakePlot();
|
||||
|
||||
act(() => {
|
||||
getHandler(config, 'init')(fakePlot);
|
||||
getHandler(config, 'setSeries')(fakePlot, 1, { focus: true });
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fakePlot.over.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(
|
||||
document
|
||||
.querySelector('.tooltip-plugin-container')
|
||||
?.classList.contains('pinned'),
|
||||
).toBe(true);
|
||||
|
||||
// Press a key outside the tooltip.
|
||||
act(() => {
|
||||
document.body.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
|
||||
);
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Cursor sync ------------------------------------------------------------
|
||||
|
||||
describe('cursor sync', () => {
|
||||
it('enables uPlot cursor sync for time-based scales when mode is Tooltip', () => {
|
||||
const config = createConfigMock({
|
||||
scales: [{ props: { time: true } }],
|
||||
} as any);
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: () => null,
|
||||
syncMode: DashboardCursorSync.Tooltip,
|
||||
syncKey: 'dashboard-sync',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(config.setCursor).toHaveBeenCalledWith({
|
||||
sync: { key: 'dashboard-sync', scales: ['x', null] },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not enable cursor sync when mode is None', () => {
|
||||
const config = createConfigMock({
|
||||
scales: [{ props: { time: true } }],
|
||||
} as any);
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: () => null,
|
||||
syncMode: DashboardCursorSync.None,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(config.setCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not enable cursor sync when scale is not time-based', () => {
|
||||
const config = createConfigMock({
|
||||
scales: [{ props: { time: false } }],
|
||||
} as any);
|
||||
|
||||
render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: () => null,
|
||||
syncMode: DashboardCursorSync.Tooltip,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(config.setCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Cleanup ----------------------------------------------------------------
|
||||
|
||||
describe('cleanup on unmount', () => {
|
||||
it('removes window event listeners and all uPlot hooks', () => {
|
||||
const config = createConfigMock();
|
||||
const addSpy = jest.spyOn(window, 'addEventListener');
|
||||
const removeSpy = jest.spyOn(window, 'removeEventListener');
|
||||
|
||||
const { unmount } = render(
|
||||
React.createElement(TooltipPlugin, {
|
||||
config: config as any,
|
||||
render: () => null,
|
||||
syncMode: DashboardCursorSync.None,
|
||||
}),
|
||||
);
|
||||
|
||||
const resizeCall = addSpy.mock.calls.find(([type]) => type === 'resize');
|
||||
const scrollCall = addSpy.mock.calls.find(([type]) => type === 'scroll');
|
||||
|
||||
expect(resizeCall).toBeDefined();
|
||||
expect(scrollCall).toBeDefined();
|
||||
|
||||
const resizeListener = resizeCall?.[1] as EventListener;
|
||||
const scrollListener = scrollCall?.[1] as EventListener;
|
||||
const scrollOptions = scrollCall?.[2];
|
||||
|
||||
unmount();
|
||||
|
||||
config.removeCallbacks.forEach((removeFn) => {
|
||||
expect(removeFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(removeSpy).toHaveBeenCalledWith('resize', resizeListener);
|
||||
expect(removeSpy).toHaveBeenCalledWith(
|
||||
'scroll',
|
||||
scrollListener,
|
||||
scrollOptions,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -412,16 +412,14 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
});
|
||||
|
||||
// Verify dashboard state contains the variables with default values
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables).toHaveProperty('environment');
|
||||
expect(parsedVariables).toHaveProperty('services');
|
||||
// Default allSelected values should be preserved
|
||||
expect(parsedVariables.environment.allSelected).toBe(false);
|
||||
expect(parsedVariables.services.allSelected).toBe(false);
|
||||
});
|
||||
expect(parsedVariables).toHaveProperty('environment');
|
||||
expect(parsedVariables).toHaveProperty('services');
|
||||
// Default allSelected values should be preserved
|
||||
expect(parsedVariables.environment.allSelected).toBe(false);
|
||||
expect(parsedVariables.services.allSelected).toBe(false);
|
||||
});
|
||||
|
||||
it('should merge URL variables with dashboard data and normalize values correctly', async () => {
|
||||
@@ -468,26 +466,16 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
});
|
||||
|
||||
// Verify the dashboard state reflects the normalized URL values
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
// First ensure the variables exist
|
||||
expect(parsedVariables).toHaveProperty('environment');
|
||||
expect(parsedVariables).toHaveProperty('services');
|
||||
// The selectedValue should be updated with normalized URL values
|
||||
expect(parsedVariables.environment.selectedValue).toBe('development');
|
||||
expect(parsedVariables.services.selectedValue).toEqual(['db', 'cache']);
|
||||
|
||||
// Then check their properties
|
||||
expect(parsedVariables.environment).toHaveProperty('selectedValue');
|
||||
expect(parsedVariables.services).toHaveProperty('selectedValue');
|
||||
|
||||
// The selectedValue should be updated with normalized URL values
|
||||
expect(parsedVariables.environment.selectedValue).toBe('development');
|
||||
expect(parsedVariables.services.selectedValue).toEqual(['db', 'cache']);
|
||||
|
||||
// allSelected should be set to false when URL values override
|
||||
expect(parsedVariables.environment.allSelected).toBe(false);
|
||||
expect(parsedVariables.services.allSelected).toBe(false);
|
||||
});
|
||||
// allSelected should be set to false when URL values override
|
||||
expect(parsedVariables.environment.allSelected).toBe(false);
|
||||
expect(parsedVariables.services.allSelected).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle ALL_SELECTED_VALUE from URL and set allSelected correctly', async () => {
|
||||
@@ -512,8 +500,8 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
);
|
||||
|
||||
// Verify that allSelected is set to true for the services variable
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
await waitFor(async () => {
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables.services.allSelected).toBe(true);
|
||||
@@ -615,8 +603,8 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
});
|
||||
|
||||
// Verify that defaultValue is set from textboxValue
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
await waitFor(async () => {
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables.myTextbox.type).toBe('TEXTBOX');
|
||||
@@ -660,8 +648,8 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
});
|
||||
|
||||
// Verify that existing defaultValue is preserved
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
await waitFor(async () => {
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables.myTextbox.type).toBe('TEXTBOX');
|
||||
@@ -706,8 +694,8 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
});
|
||||
|
||||
// Verify that defaultValue is set to empty string
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
await waitFor(async () => {
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables.myTextbox.type).toBe('TEXTBOX');
|
||||
@@ -751,8 +739,8 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
});
|
||||
|
||||
// Verify that defaultValue is NOT set from textboxValue for QUERY type
|
||||
await waitFor(() => {
|
||||
const dashboardVariables = screen.getByTestId('dashboard-variables');
|
||||
await waitFor(async () => {
|
||||
const dashboardVariables = await screen.findByTestId('dashboard-variables');
|
||||
const parsedVariables = JSON.parse(dashboardVariables.textContent || '{}');
|
||||
|
||||
expect(parsedVariables.myQuery.type).toBe('QUERY');
|
||||
|
||||
@@ -247,8 +247,6 @@ func FilterResponse(results []*qbtypes.QueryRangeResponse) []*qbtypes.QueryRange
|
||||
}
|
||||
}
|
||||
resultData.Rows = filteredRows
|
||||
case *qbtypes.ScalarData:
|
||||
resultData.Data = filterScalarDataIPs(resultData.Columns, resultData.Data)
|
||||
}
|
||||
|
||||
filteredData = append(filteredData, result)
|
||||
@@ -274,39 +272,6 @@ func shouldIncludeSeries(series *qbtypes.TimeSeries) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func filterScalarDataIPs(columns []*qbtypes.ColumnDescriptor, data [][]any) [][]any {
|
||||
// Find column indices for server address fields
|
||||
serverColIndices := make([]int, 0)
|
||||
for i, col := range columns {
|
||||
if col.Name == serverAddressKeyLegacy || col.Name == serverAddressKey {
|
||||
serverColIndices = append(serverColIndices, i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(serverColIndices) == 0 {
|
||||
return data
|
||||
}
|
||||
|
||||
filtered := make([][]any, 0, len(data))
|
||||
for _, row := range data {
|
||||
includeRow := true
|
||||
for _, colIdx := range serverColIndices {
|
||||
if colIdx < len(row) {
|
||||
if strVal, ok := row[colIdx].(string); ok {
|
||||
if net.ParseIP(strVal) != nil {
|
||||
includeRow = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if includeRow {
|
||||
filtered = append(filtered, row)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func shouldIncludeRow(row *qbtypes.RawRow) bool {
|
||||
if row.Data != nil {
|
||||
for _, key := range []string{serverAddressKeyLegacy, serverAddressKey} {
|
||||
|
||||
@@ -116,59 +116,6 @@ func TestFilterResponse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should filter out IP addresses from scalar data",
|
||||
input: []*qbtypes.QueryRangeResponse{
|
||||
{
|
||||
Data: qbtypes.QueryData{
|
||||
Results: []any{
|
||||
&qbtypes.ScalarData{
|
||||
QueryName: "endpoints",
|
||||
Columns: []*qbtypes.ColumnDescriptor{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
|
||||
Type: qbtypes.ColumnTypeGroup,
|
||||
},
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "endpoints"},
|
||||
Type: qbtypes.ColumnTypeAggregation,
|
||||
},
|
||||
},
|
||||
Data: [][]any{
|
||||
{"192.168.1.1", 10},
|
||||
{"example.com", 20},
|
||||
{"10.0.0.1", 5},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*qbtypes.QueryRangeResponse{
|
||||
{
|
||||
Data: qbtypes.QueryData{
|
||||
Results: []any{
|
||||
&qbtypes.ScalarData{
|
||||
QueryName: "endpoints",
|
||||
Columns: []*qbtypes.ColumnDescriptor{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "net.peer.name"},
|
||||
Type: qbtypes.ColumnTypeGroup,
|
||||
},
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "endpoints"},
|
||||
Type: qbtypes.ColumnTypeAggregation,
|
||||
},
|
||||
},
|
||||
Data: [][]any{
|
||||
{"example.com", 20},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -208,16 +208,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
event.GroupByApplied = len(spec.GroupBy) > 0
|
||||
|
||||
if spec.Source == telemetrytypes.SourceMeter {
|
||||
if spec.StepInterval.Seconds() == 0 {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMeter(req.Start, req.End))}
|
||||
}
|
||||
|
||||
if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepIntervalForMeter(req.Start, req.End)) {
|
||||
newStep := qbtypes.Step{
|
||||
Duration: time.Second * time.Duration(querybuilder.MinAllowedStepIntervalForMeter(req.Start, req.End)),
|
||||
}
|
||||
spec.StepInterval = newStep
|
||||
}
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMeter(req.Start, req.End))}
|
||||
} else {
|
||||
if spec.StepInterval.Seconds() == 0 {
|
||||
spec.StepInterval = qbtypes.Step{
|
||||
|
||||
@@ -89,23 +89,6 @@ func RecommendedStepIntervalForMeter(start, end uint64) uint64 {
|
||||
return recommended
|
||||
}
|
||||
|
||||
func MinAllowedStepIntervalForMeter(start, end uint64) uint64 {
|
||||
start = ToNanoSecs(start)
|
||||
end = ToNanoSecs(end)
|
||||
|
||||
step := (end - start) / RecommendedNumberOfPoints / 1e9
|
||||
|
||||
// for meter queries the minimum step interval allowed is 1 hour as this is our granularity
|
||||
if step < 3600 {
|
||||
return 3600
|
||||
}
|
||||
|
||||
// return the nearest lower multiple of 3600 ( 1 hour )
|
||||
minAllowed := step - step%3600
|
||||
|
||||
return minAllowed
|
||||
}
|
||||
|
||||
func RecommendedStepIntervalForMetric(start, end uint64) uint64 {
|
||||
start = ToNanoSecs(start)
|
||||
end = ToNanoSecs(end)
|
||||
|
||||
Reference in New Issue
Block a user