Compare commits

..

3 Commits

Author SHA1 Message Date
Abhi kumar
6156156a85 Merge branch 'main' into test/tooltip-plugin 2026-02-09 20:00:08 +05:30
Abhi kumar
fb0d2830ae Merge branch 'main' into test/tooltip-plugin 2026-02-09 19:34:26 +05:30
Abhi Kumar
04e5d87986 test: added test for tooltip plugin 2026-02-09 13:16:20 +05:30
12 changed files with 514 additions and 243 deletions

View File

@@ -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',
},
],

View File

@@ -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>
)}

View File

@@ -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);

View File

@@ -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}>

View File

@@ -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);
});
});
});

View File

@@ -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',
];

View 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,
);
});
});
});

View File

@@ -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');

View File

@@ -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} {

View File

@@ -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 {

View File

@@ -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{

View File

@@ -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)