mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
chore: automatically show query addon when the value is present even after refresh (#9024)
* chore: automatically show query addon when the value is present even after refresh * chore: minor cleanup * test: added tests for queryAddon * test: removed inputwithlabel mock
This commit is contained in:
parent
31e042adf7
commit
b1ea7eab70
@ -49,6 +49,7 @@ function InputWithLabel({
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
name={label.toLowerCase()}
|
||||
data-testid={`input-${label}`}
|
||||
/>
|
||||
{labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
|
||||
{onClose && (
|
||||
|
||||
@ -9,7 +9,7 @@ import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/Orde
|
||||
import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { get, isEmpty } from 'lodash-es';
|
||||
import { BarChart2, ChevronUp, ExternalLink, ScrollText } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@ -34,6 +34,14 @@ const ADD_ONS_KEYS = {
|
||||
LEGEND_FORMAT: 'legend_format',
|
||||
};
|
||||
|
||||
const ADD_ONS_KEYS_TO_QUERY_PATH = {
|
||||
[ADD_ONS_KEYS.GROUP_BY]: 'groupBy',
|
||||
[ADD_ONS_KEYS.HAVING]: 'having.expression',
|
||||
[ADD_ONS_KEYS.ORDER_BY]: 'orderBy',
|
||||
[ADD_ONS_KEYS.LIMIT]: 'limit',
|
||||
[ADD_ONS_KEYS.LEGEND_FORMAT]: 'legend',
|
||||
};
|
||||
|
||||
const ADD_ONS = [
|
||||
{
|
||||
icon: <BarChart2 size={14} />,
|
||||
@ -91,6 +99,9 @@ const REDUCE_TO = {
|
||||
'https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations',
|
||||
};
|
||||
|
||||
const hasValue = (value: unknown): boolean =>
|
||||
value != null && value !== '' && !(Array.isArray(value) && value.length === 0);
|
||||
|
||||
// Custom tooltip content component
|
||||
function TooltipContent({
|
||||
label,
|
||||
@ -195,21 +206,29 @@ function QueryAddOns({
|
||||
}
|
||||
}
|
||||
|
||||
// add reduce to if showReduceTo is true
|
||||
if (showReduceTo) {
|
||||
filteredAddOns = [...filteredAddOns, REDUCE_TO];
|
||||
}
|
||||
|
||||
setAddOns(filteredAddOns);
|
||||
|
||||
// Filter selectedViews to only include add-ons present in filteredAddOns
|
||||
setSelectedViews((prevSelectedViews) =>
|
||||
prevSelectedViews.filter((view) =>
|
||||
filteredAddOns.some((addOn) => addOn.key === view.key),
|
||||
const activeAddOnKeys = new Set(
|
||||
Object.entries(ADD_ONS_KEYS_TO_QUERY_PATH)
|
||||
.filter(([, path]) => hasValue(get(query, path)))
|
||||
.map(([key]) => key),
|
||||
);
|
||||
|
||||
const availableAddOnKeys = new Set(filteredAddOns.map((addOn) => addOn.key));
|
||||
|
||||
// Filter and set selected views: add-ons that are both active and available
|
||||
setSelectedViews(
|
||||
ADD_ONS.filter(
|
||||
(addOn) =>
|
||||
activeAddOnKeys.has(addOn.key) && availableAddOnKeys.has(addOn.key),
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [panelType, isListViewPanel, query.dataSource]);
|
||||
}, [panelType, isListViewPanel, query]);
|
||||
|
||||
const handleOptionClick = (e: RadioChangeEvent): void => {
|
||||
if (selectedViews.find((view) => view.key === e.target.value.key)) {
|
||||
@ -285,7 +304,7 @@ function QueryAddOns({
|
||||
{selectedViews.length > 0 && (
|
||||
<div className="selected-add-ons-content">
|
||||
{selectedViews.find((view) => view.key === 'group_by') && (
|
||||
<div className="add-on-content">
|
||||
<div className="add-on-content" data-testid="group-by-content">
|
||||
<div className="periscope-input-with-label">
|
||||
<Tooltip
|
||||
title={
|
||||
@ -321,7 +340,7 @@ function QueryAddOns({
|
||||
</div>
|
||||
)}
|
||||
{selectedViews.find((view) => view.key === 'having') && (
|
||||
<div className="add-on-content">
|
||||
<div className="add-on-content" data-testid="having-content">
|
||||
<div className="periscope-input-with-label">
|
||||
<Tooltip
|
||||
title={
|
||||
@ -353,7 +372,7 @@ function QueryAddOns({
|
||||
</div>
|
||||
)}
|
||||
{selectedViews.find((view) => view.key === 'limit') && (
|
||||
<div className="add-on-content">
|
||||
<div className="add-on-content" data-testid="limit-content">
|
||||
<InputWithLabel
|
||||
label="Limit"
|
||||
onChange={handleChangeLimit}
|
||||
@ -367,7 +386,7 @@ function QueryAddOns({
|
||||
</div>
|
||||
)}
|
||||
{selectedViews.find((view) => view.key === 'order_by') && (
|
||||
<div className="add-on-content">
|
||||
<div className="add-on-content" data-testid="order-by-content">
|
||||
<div className="periscope-input-with-label">
|
||||
<Tooltip
|
||||
title={
|
||||
@ -405,7 +424,7 @@ function QueryAddOns({
|
||||
)}
|
||||
|
||||
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
|
||||
<div className="add-on-content">
|
||||
<div className="add-on-content" data-testid="reduce-to-content">
|
||||
<div className="periscope-input-with-label">
|
||||
<Tooltip
|
||||
title={
|
||||
@ -436,7 +455,7 @@ function QueryAddOns({
|
||||
)}
|
||||
|
||||
{selectedViews.find((view) => view.key === 'legend_format') && (
|
||||
<div className="add-on-content">
|
||||
<div className="add-on-content" data-testid="legend-format-content">
|
||||
<InputWithLabel
|
||||
label="Legend format"
|
||||
placeholder="Write legend format"
|
||||
|
||||
@ -0,0 +1,186 @@
|
||||
/* eslint-disable */
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import QueryAddOns from '../QueryV2/QueryAddOns/QueryAddOns';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
// Mocks: only what is required for this component to render and for us to assert handler calls
|
||||
const mockHandleChangeQueryData = jest.fn();
|
||||
const mockHandleSetQueryData = jest.fn();
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilderOperations', () => ({
|
||||
useQueryOperations: () => ({
|
||||
handleChangeQueryData: mockHandleChangeQueryData,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: () => ({
|
||||
handleSetQueryData: mockHandleSetQueryData,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/QueryBuilder/filters/GroupByFilter/GroupByFilter', () => ({
|
||||
GroupByFilter: ({ onChange }: any) => (
|
||||
<button data-testid="groupby" onClick={() => onChange(['service.name'])}>
|
||||
GroupByFilter
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('container/QueryBuilder/filters/OrderByFilter/OrderByFilter', () => ({
|
||||
OrderByFilter: ({ onChange }: any) => (
|
||||
<button
|
||||
data-testid="orderby"
|
||||
onClick={() => onChange([{ columnName: 'duration', order: 'desc' }])}
|
||||
>
|
||||
OrderByFilter
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../QueryV2/QueryAddOns/HavingFilter/HavingFilter', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onChange, onClose }: any) => (
|
||||
<div>
|
||||
<button data-testid="having-change" onClick={() => onChange('p99 > 500')}>
|
||||
HavingFilter
|
||||
</button>
|
||||
<button data-testid="having-close" onClick={onClose}>
|
||||
close
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter',
|
||||
() => ({
|
||||
ReduceToFilter: ({ onChange }: any) => (
|
||||
<button data-testid="reduce-to" onClick={() => onChange('sum')}>
|
||||
ReduceToFilter
|
||||
</button>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
function baseQuery(overrides: Partial<any> = {}): any {
|
||||
return {
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregations: [{ id: 'a', operator: 'count' }],
|
||||
groupBy: [],
|
||||
orderBy: [],
|
||||
legend: '',
|
||||
limit: null,
|
||||
having: { expression: '' },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('QueryAddOns', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('VALUE panel: no sections auto-open when query has no active add-ons', () => {
|
||||
render(
|
||||
<QueryAddOns
|
||||
query={baseQuery()}
|
||||
version="v5"
|
||||
isListViewPanel={false}
|
||||
showReduceTo
|
||||
panelType={PANEL_TYPES.VALUE}
|
||||
index={0}
|
||||
isForTraceOperator={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('legend-format-content')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('reduce-to-content')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('order-by-content')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('limit-content')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('group-by-content')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('having-content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides group-by section for METRICS even if groupBy is set in query', () => {
|
||||
render(
|
||||
<QueryAddOns
|
||||
query={baseQuery({
|
||||
dataSource: DataSource.METRICS,
|
||||
groupBy: ['service.name'],
|
||||
})}
|
||||
version="v5"
|
||||
isListViewPanel={false}
|
||||
showReduceTo={false}
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
index={0}
|
||||
isForTraceOperator={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('group-by-content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('defaults to Order By open in list view panel', () => {
|
||||
render(
|
||||
<QueryAddOns
|
||||
query={baseQuery()}
|
||||
version="v5"
|
||||
isListViewPanel
|
||||
showReduceTo={false}
|
||||
panelType={PANEL_TYPES.LIST}
|
||||
index={0}
|
||||
isForTraceOperator={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('order-by-content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('limit input auto-opens when limit is set and changing it calls handler', () => {
|
||||
render(
|
||||
<QueryAddOns
|
||||
query={baseQuery({ limit: 5 })}
|
||||
version="v5"
|
||||
isListViewPanel={false}
|
||||
showReduceTo={false}
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
index={0}
|
||||
isForTraceOperator={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('input-Limit') as HTMLInputElement;
|
||||
expect(screen.getByTestId('limit-content')).toBeInTheDocument();
|
||||
expect(input.value).toBe('5');
|
||||
|
||||
fireEvent.change(input, { target: { value: '10' } });
|
||||
expect(mockHandleChangeQueryData).toHaveBeenCalledWith('limit', 10);
|
||||
});
|
||||
|
||||
it('auto-opens Order By and Limit when present in query', () => {
|
||||
const query = baseQuery({
|
||||
orderBy: [{ columnName: 'duration', order: 'desc' }],
|
||||
limit: 7,
|
||||
});
|
||||
render(
|
||||
<QueryAddOns
|
||||
query={query}
|
||||
version="v5"
|
||||
isListViewPanel={false}
|
||||
showReduceTo={false}
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
index={0}
|
||||
isForTraceOperator={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('order-by-content')).toBeInTheDocument();
|
||||
const limitInput = screen.getByTestId('input-Limit') as HTMLInputElement;
|
||||
expect(screen.getByTestId('limit-content')).toBeInTheDocument();
|
||||
expect(limitInput.value).toBe('7');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user