mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
fix: y axis unit not interactive in the panel edit mode (#9003)
This commit is contained in:
parent
2dbe0777f4
commit
bcd21cee74
@ -73,7 +73,6 @@ export function ColumnUnitSelector(
|
|||||||
<Typography.Text className="heading">Column Units</Typography.Text>
|
<Typography.Text className="heading">Column Units</Typography.Text>
|
||||||
{aggregationQueries.map(({ value, label }) => (
|
{aggregationQueries.map(({ value, label }) => (
|
||||||
<YAxisUnitSelector
|
<YAxisUnitSelector
|
||||||
defaultValue={columnUnits[value]}
|
|
||||||
value={columnUnits[value] || ''}
|
value={columnUnits[value] || ''}
|
||||||
onSelect={(unitValue: string): void =>
|
onSelect={(unitValue: string): void =>
|
||||||
handleColumnUnitSelect(value, unitValue)
|
handleColumnUnitSelect(value, unitValue)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { AutoComplete, Input, Typography } from 'antd';
|
import { AutoComplete, Input, Typography } from 'antd';
|
||||||
import { find } from 'lodash-es';
|
import { find } from 'lodash-es';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { flattenedCategories } from './dataFormatCategories';
|
import { flattenedCategories } from './dataFormatCategories';
|
||||||
|
|
||||||
@ -14,25 +14,54 @@ const findCategoryByName = (
|
|||||||
find(flattenedCategories, (option) => option.name === searchValue);
|
find(flattenedCategories, (option) => option.name === searchValue);
|
||||||
|
|
||||||
type OnSelectType = Dispatch<SetStateAction<string>> | ((val: string) => void);
|
type OnSelectType = Dispatch<SetStateAction<string>> | ((val: string) => void);
|
||||||
|
|
||||||
function YAxisUnitSelector({
|
function YAxisUnitSelector({
|
||||||
defaultValue,
|
|
||||||
value,
|
value,
|
||||||
onSelect,
|
onSelect,
|
||||||
fieldLabel,
|
fieldLabel,
|
||||||
handleClear,
|
handleClear,
|
||||||
}: {
|
}: {
|
||||||
defaultValue: string;
|
|
||||||
value: string;
|
value: string;
|
||||||
onSelect: OnSelectType;
|
onSelect: OnSelectType;
|
||||||
fieldLabel: string;
|
fieldLabel: string;
|
||||||
handleClear?: () => void;
|
handleClear?: () => void;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
|
// Sync input value with the actual value prop
|
||||||
|
useEffect(() => {
|
||||||
|
const category = findCategoryById(value);
|
||||||
|
setInputValue(category?.name || '');
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const onSelectHandler = (selectedValue: string): void => {
|
const onSelectHandler = (selectedValue: string): void => {
|
||||||
onSelect(findCategoryByName(selectedValue)?.id || '');
|
const category = findCategoryByName(selectedValue);
|
||||||
|
if (category) {
|
||||||
|
onSelect(category.id);
|
||||||
|
setInputValue(selectedValue);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeHandler = (inputValue: string): void => {
|
||||||
|
setInputValue(inputValue);
|
||||||
|
// Clear the yAxisUnit if input is empty or doesn't match any option
|
||||||
|
if (!inputValue) {
|
||||||
|
onSelect('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClearHandler = (): void => {
|
||||||
|
setInputValue('');
|
||||||
|
onSelect('');
|
||||||
|
if (handleClear) {
|
||||||
|
handleClear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const options = flattenedCategories.map((options) => ({
|
const options = flattenedCategories.map((options) => ({
|
||||||
value: options.name,
|
value: options.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="y-axis-unit-selector">
|
<div className="y-axis-unit-selector">
|
||||||
<Typography.Text className="heading">{fieldLabel}</Typography.Text>
|
<Typography.Text className="heading">{fieldLabel}</Typography.Text>
|
||||||
@ -41,9 +70,9 @@ function YAxisUnitSelector({
|
|||||||
rootClassName="y-axis-root-popover"
|
rootClassName="y-axis-root-popover"
|
||||||
options={options}
|
options={options}
|
||||||
allowClear
|
allowClear
|
||||||
defaultValue={findCategoryById(defaultValue)?.name}
|
value={inputValue}
|
||||||
value={findCategoryById(value)?.name || ''}
|
onChange={onChangeHandler}
|
||||||
onClear={handleClear}
|
onClear={onClearHandler}
|
||||||
onSelect={onSelectHandler}
|
onSelect={onSelectHandler}
|
||||||
filterOption={(inputValue, option): boolean => {
|
filterOption={(inputValue, option): boolean => {
|
||||||
if (option) {
|
if (option) {
|
||||||
|
|||||||
@ -0,0 +1,240 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
import YAxisUnitSelector from '../YAxisUnitSelector';
|
||||||
|
|
||||||
|
// Mock the dataFormatCategories to have predictable test data
|
||||||
|
jest.mock('../dataFormatCategories', () => ({
|
||||||
|
flattenedCategories: [
|
||||||
|
{ id: 'seconds', name: 'seconds (s)' },
|
||||||
|
{ id: 'milliseconds', name: 'milliseconds (ms)' },
|
||||||
|
{ id: 'hours', name: 'hours (h)' },
|
||||||
|
{ id: 'minutes', name: 'minutes (m)' },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const MOCK_SECONDS = 'seconds';
|
||||||
|
const MOCK_MILLISECONDS = 'milliseconds';
|
||||||
|
|
||||||
|
describe('YAxisUnitSelector', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
value: MOCK_SECONDS,
|
||||||
|
onSelect: jest.fn(),
|
||||||
|
fieldLabel: 'Y Axis Unit',
|
||||||
|
handleClear: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user: ReturnType<typeof userEvent.setup>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
user = userEvent.setup();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Rendering (Read) & (write)', () => {
|
||||||
|
it('renders with correct field label', () => {
|
||||||
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value={defaultProps.value}
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Y Axis Unit')).toBeInTheDocument();
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
expect(input).toHaveValue('seconds (s)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders with custom field label', () => {
|
||||||
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value={defaultProps.value}
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel="Custom Unit Label"
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Custom Unit Label')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays empty input when value prop is empty', () => {
|
||||||
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value=""
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByDisplayValue('')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows placeholder text', () => {
|
||||||
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value={defaultProps.value}
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByPlaceholderText('Unit')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles numeric input', async () => {
|
||||||
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value={defaultProps.value}
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
await user.clear(input);
|
||||||
|
await user.type(input, '12345');
|
||||||
|
expect(input).toHaveValue('12345');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles mixed content input', async () => {
|
||||||
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value={defaultProps.value}
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
await user.clear(input);
|
||||||
|
await user.type(input, 'Test123!@#');
|
||||||
|
expect(input).toHaveValue('Test123!@#');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('State Management', () => {
|
||||||
|
it('syncs input value with value prop changes', async () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value={defaultProps.value}
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
// Initial value
|
||||||
|
expect(input).toHaveValue('seconds (s)');
|
||||||
|
|
||||||
|
// Change value prop
|
||||||
|
rerender(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value={MOCK_MILLISECONDS}
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(input).toHaveValue('milliseconds (ms)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty value prop correctly', async () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value={defaultProps.value}
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
// Change to empty value
|
||||||
|
rerender(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value=""
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(input).toHaveValue('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles invalid value prop gracefully', async () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value={defaultProps.value}
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
// Change to invalid value
|
||||||
|
rerender(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value="invalid_id"
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(input).toHaveValue('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maintains local state during typing', async () => {
|
||||||
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value={defaultProps.value}
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
// first clear then type
|
||||||
|
await user.clear(input);
|
||||||
|
await user.type(input, 'test');
|
||||||
|
expect(input).toHaveValue('test');
|
||||||
|
|
||||||
|
// Value prop change should not override local typing
|
||||||
|
await act(async () => {
|
||||||
|
// Simulate prop change
|
||||||
|
render(
|
||||||
|
<YAxisUnitSelector
|
||||||
|
value="bytes"
|
||||||
|
onSelect={defaultProps.onSelect}
|
||||||
|
fieldLabel={defaultProps.fieldLabel}
|
||||||
|
handleClear={defaultProps.handleClear}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Local typing should be preserved
|
||||||
|
expect(input).toHaveValue('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -337,7 +337,6 @@ function RightContainer({
|
|||||||
|
|
||||||
{allowYAxisUnit && (
|
{allowYAxisUnit && (
|
||||||
<YAxisUnitSelector
|
<YAxisUnitSelector
|
||||||
defaultValue={yAxisUnit}
|
|
||||||
onSelect={setYAxisUnit}
|
onSelect={setYAxisUnit}
|
||||||
value={yAxisUnit || ''}
|
value={yAxisUnit || ''}
|
||||||
fieldLabel={
|
fieldLabel={
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user