mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 18:35:58 +08:00
fix: fix 'open in explorer' functionality in metrics explorer (#7873)
This commit is contained in:
parent
c7db85f44c
commit
727a039eb9
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user