fix: fix 'open in explorer' functionality in metrics explorer (#7873)

This commit is contained in:
Amlan Kumar Nandy 2025-05-09 11:34:32 +07:00 committed by GitHub
parent c7db85f44c
commit 727a039eb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 204 additions and 53 deletions

View File

@ -3,28 +3,40 @@ import { ColumnsType } from 'antd/es/table';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import { DataType } from 'container/LogDetailedView/TableView'; import { DataType } from 'container/LogDetailedView/TableView';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { AllAttributesProps } from './types'; import { AllAttributesProps } from './types';
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
import { getMetricDetailsQuery } from './utils';
import ROUTES from '../../../constants/routes';
import { PANEL_TYPES } from '../../../constants/queryBuilder';
function AllAttributes({ attributes }: AllAttributesProps): JSX.Element { function AllAttributes({
metricName,
attributes,
}: AllAttributesProps): JSX.Element {
const [searchString, setSearchString] = useState(''); const [searchString, setSearchString] = useState('');
const [activeKey, setActiveKey] = useState<string | string[]>( const [activeKey, setActiveKey] = useState<string | string[]>(
'all-attributes', 'all-attributes',
); );
// const { safeNavigate } = useSafeNavigate(); const { handleExplorerTabChange } = useHandleExplorerTabChange();
// const goToMetricsExploreWithAppliedAttribute = useCallback( const goToMetricsExploreWithAppliedAttribute = useCallback(
// (key: string, value: string) => { (key: string, value: string) => {
// const compositeQuery = getMetricDetailsQuery(metricName, { key, value }); const compositeQuery = getMetricDetailsQuery(metricName, { key, value });
// const encodedCompositeQuery = JSON.stringify(compositeQuery); handleExplorerTabChange(
// safeNavigate( PANEL_TYPES.TIME_SERIES,
// `${ROUTES.METRICS_EXPLORER_EXPLORER}?compositeQuery=${encodedCompositeQuery}`, {
// ); query: compositeQuery,
// }, name: metricName,
// [metricName, safeNavigate], id: metricName,
// ); },
ROUTES.METRICS_EXPLORER_EXPLORER,
);
},
[metricName, handleExplorerTabChange],
);
const filteredAttributes = useMemo( const filteredAttributes = useMemo(
() => () =>
@ -81,10 +93,9 @@ function AllAttributes({ attributes }: AllAttributesProps): JSX.Element {
<Button <Button
key={attribute} key={attribute}
type="text" type="text"
// TODO: Enable this once we have fixed the redirect issue onClick={(): void => {
// onClick={(): void => { goToMetricsExploreWithAppliedAttribute(field.key, attribute);
// goToMetricsExploreWithAppliedAttribute(field.key, attribute); }}
// }}
> >
<Typography.Text>{attribute}</Typography.Text> <Typography.Text>{attribute}</Typography.Text>
</Button> </Button>
@ -93,7 +104,7 @@ function AllAttributes({ attributes }: AllAttributesProps): JSX.Element {
), ),
}, },
], ],
[], [goToMetricsExploreWithAppliedAttribute],
); );
const items = useMemo( const items = useMemo(

View File

@ -14,6 +14,11 @@
} }
} }
.metric-details-header-buttons {
display: flex;
gap: 8px;
}
.ant-btn { .ant-btn {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -13,8 +13,8 @@ import {
} from 'antd'; } from 'antd';
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails'; import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { Compass, X } from 'lucide-react'; import { Compass, Crosshair, X } from 'lucide-react';
import { useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { isInspectEnabled } from '../Inspect/utils'; import { isInspectEnabled } from '../Inspect/utils';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils'; import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
@ -25,7 +25,11 @@ import { MetricDetailsProps } from './types';
import { import {
formatNumberToCompactFormat, formatNumberToCompactFormat,
formatTimestampToReadableDate, formatTimestampToReadableDate,
getMetricDetailsQuery,
} from './utils'; } from './utils';
import { PANEL_TYPES } from '../../../constants/queryBuilder';
import ROUTES from '../../../constants/routes';
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
function MetricDetails({ function MetricDetails({
onClose, onClose,
@ -34,7 +38,7 @@ function MetricDetails({
openInspectModal, openInspectModal,
}: MetricDetailsProps): JSX.Element { }: MetricDetailsProps): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
// const { safeNavigate } = useSafeNavigate(); const { handleExplorerTabChange } = useHandleExplorerTabChange();
const { const {
data, data,
@ -76,15 +80,20 @@ function MetricDetails({
); );
}, [metric]); }, [metric]);
// const goToMetricsExplorerwithSelectedMetric = useCallback(() => { const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
// if (metricName) { if (metricName) {
// const compositeQuery = getMetricDetailsQuery(metricName); const compositeQuery = getMetricDetailsQuery(metricName);
// const encodedCompositeQuery = JSON.stringify(compositeQuery); handleExplorerTabChange(
// safeNavigate( PANEL_TYPES.TIME_SERIES,
// `${ROUTES.METRICS_EXPLORER_EXPLORER}?compositeQuery=${encodedCompositeQuery}`, {
// ); query: compositeQuery,
// } name: metricName,
// }, [metricName, safeNavigate]); id: metricName,
},
ROUTES.METRICS_EXPLORER_EXPLORER,
);
}
}, [metricName, handleExplorerTabChange]);
const isMetricDetailsError = metricDetailsError || !metric; const isMetricDetailsError = metricDetailsError || !metric;
@ -97,30 +106,31 @@ function MetricDetails({
<Divider type="vertical" /> <Divider type="vertical" />
<Typography.Text>{metric?.name}</Typography.Text> <Typography.Text>{metric?.name}</Typography.Text>
</div> </div>
{/* TODO: Enable this once we have fixed the redirect issue */} <div className="metric-details-header-buttons">
{/* <Button <Button
onClick={goToMetricsExplorerwithSelectedMetric} onClick={goToMetricsExplorerwithSelectedMetric}
icon={<Compass size={16} />} icon={<Compass size={16} />}
disabled={!metricName} disabled={!metricName}
target="_blank" data-testid="open-in-explorer-button"
rel="noopener noreferrer"
> >
Open in Explorer Open in Explorer
</Button> */} </Button>
{/* Show the based on the feature flag. Will remove before releasing the feature */} {/* Show the based on the feature flag. Will remove before releasing the feature */}
{showInspectFeature && ( {showInspectFeature && (
<Button <Button
className="inspect-metrics-button" className="inspect-metrics-button"
aria-label="Inspect Metric" aria-label="Inspect Metric"
icon={<Compass size={18} />} icon={<Crosshair size={18} />}
onClick={(): void => { onClick={(): void => {
if (metric?.name) { if (metric?.name) {
openInspectModal(metric.name); openInspectModal(metric.name);
} }
}} }}
data-testid="inspect-metric-button"
/> />
)} )}
</div> </div>
</div>
} }
placement="right" placement="right"
onClose={onClose} onClose={onClose}

View File

@ -0,0 +1,82 @@
import { fireEvent, render, screen } from '@testing-library/react';
import AllAttributes from '../AllAttributes';
import { MetricDetailsAttribute } from '../../../../api/metricsExplorer/getMetricDetails';
import ROUTES from '../../../../constants/routes';
import * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${ROUTES.METRICS_EXPLORER}`,
}),
}));
const mockHandleExplorerTabChange = jest.fn();
jest
.spyOn(useHandleExplorerTabChange, 'useHandleExplorerTabChange')
.mockReturnValue({
handleExplorerTabChange: mockHandleExplorerTabChange,
});
const mockMetricName = 'test-metric';
const mockAttributes: MetricDetailsAttribute[] = [
{
key: 'attribute1',
value: ['value1', 'value2'],
valueCount: 2,
},
{
key: 'attribute2',
value: ['value3'],
valueCount: 1,
},
];
describe('AllAttributes', () => {
it('renders attributes section with title', () => {
render(
<AllAttributes metricName={mockMetricName} attributes={mockAttributes} />,
);
expect(screen.getByText('All Attributes')).toBeInTheDocument();
});
it('renders all attribute keys and values', () => {
render(
<AllAttributes metricName={mockMetricName} attributes={mockAttributes} />,
);
// Check attribute keys are rendered
expect(screen.getByText('attribute1')).toBeInTheDocument();
expect(screen.getByText('attribute2')).toBeInTheDocument();
// Check attribute values are rendered
expect(screen.getByText('value1')).toBeInTheDocument();
expect(screen.getByText('value2')).toBeInTheDocument();
expect(screen.getByText('value3')).toBeInTheDocument();
});
it('renders value counts correctly', () => {
render(
<AllAttributes metricName={mockMetricName} attributes={mockAttributes} />,
);
expect(screen.getByText('2')).toBeInTheDocument(); // For attribute1
expect(screen.getByText('1')).toBeInTheDocument(); // For attribute2
});
it('handles empty attributes array', () => {
render(<AllAttributes metricName={mockMetricName} attributes={[]} />);
expect(screen.getByText('All Attributes')).toBeInTheDocument();
expect(screen.queryByText('No data')).toBeInTheDocument();
});
it('clicking on an attribute value opens the explorer with the attribute filter applied', () => {
render(
<AllAttributes metricName={mockMetricName} attributes={mockAttributes} />,
);
fireEvent.click(screen.getByText('value1'));
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
});
});

View File

@ -1,9 +1,10 @@
import { render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import { MetricDetails } from 'api/metricsExplorer/getMetricDetails'; import { MetricDetails } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList'; import { MetricType } from 'api/metricsExplorer/getMetricsList';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import * as useGetMetricDetails from 'hooks/metricsExplorer/useGetMetricDetails'; import * as useGetMetricDetails from 'hooks/metricsExplorer/useGetMetricDetails';
import * as useUpdateMetricMetadata from 'hooks/metricsExplorer/useUpdateMetricMetadata'; import * as useUpdateMetricMetadata from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
import MetricDetailsView from '../MetricDetails'; import MetricDetailsView from '../MetricDetails';
@ -61,6 +62,13 @@ jest.spyOn(useUpdateMetricMetadata, 'useUpdateMetricMetadata').mockReturnValue({
error: null, error: null,
} as any); } as any);
const mockHandleExplorerTabChange = jest.fn();
jest
.spyOn(useHandleExplorerTabChange, 'useHandleExplorerTabChange')
.mockReturnValue({
handleExplorerTabChange: mockHandleExplorerTabChange,
});
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({ useLocation: (): { pathname: string } => ({
@ -90,6 +98,41 @@ describe('MetricDetails', () => {
expect(screen.getByText(`${mockMetricData.unit}`)).toBeInTheDocument(); expect(screen.getByText(`${mockMetricData.unit}`)).toBeInTheDocument();
}); });
it('renders the "open in explorer" and "inspect" buttons', () => {
jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValueOnce({
...mockUseGetMetricDetailsData,
data: {
payload: {
data: {
...mockMetricData,
metadata: {
...mockMetricData.metadata,
metric_type: MetricType.GAUGE,
},
},
},
},
} as any);
render(
<MetricDetailsView
onClose={mockOnClose}
isOpen
metricName={mockMetricName}
isModalTimeSelection
openInspectModal={mockOpenInspectModal}
/>,
);
expect(screen.getByTestId('open-in-explorer-button')).toBeInTheDocument();
expect(screen.getByTestId('inspect-metric-button')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('open-in-explorer-button'));
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
fireEvent.click(screen.getByTestId('inspect-metric-button'));
expect(mockOpenInspectModal).toHaveBeenCalled();
});
it('should render error state when metric details are not found', () => { it('should render error state when metric details are not found', () => {
jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValue({ jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValue({
...mockUseGetMetricDetailsData, ...mockUseGetMetricDetailsData,