mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-03 20:42:02 +00:00
Compare commits
12 Commits
light-mode
...
enh/extern
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91ee0fb06f | ||
|
|
6a138fa422 | ||
|
|
1d967fadac | ||
|
|
28b21f6c48 | ||
|
|
7974f4fb08 | ||
|
|
f11153cdec | ||
|
|
a380c4d6be | ||
|
|
a0f576e5fb | ||
|
|
98013ee3b9 | ||
|
|
97b94fc4f5 | ||
|
|
0f3f49b96a | ||
|
|
0928a30863 |
@@ -2,7 +2,7 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
// Handles errors from generated API hooks (which use RenderErrorResponseDTO)
|
||||
// @deprecated Use convertToApiError instead
|
||||
export function ErrorResponseHandlerForGeneratedAPIs(
|
||||
error: AxiosError<RenderErrorResponseDTO>,
|
||||
): never {
|
||||
@@ -46,3 +46,34 @@ export function ErrorResponseHandlerForGeneratedAPIs(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// convertToApiError converts an AxiosError from generated API
|
||||
// hooks into an APIError.
|
||||
export function convertToApiError(
|
||||
error: AxiosError<RenderErrorResponseDTO> | null,
|
||||
): APIError | undefined {
|
||||
if (!error) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const response = error.response;
|
||||
const errorData = response?.data?.error;
|
||||
|
||||
return new APIError({
|
||||
httpStatusCode: response?.status || error.status || 500,
|
||||
error: {
|
||||
code:
|
||||
errorData?.code ||
|
||||
String(response?.status || error.code || 'unknown_error'),
|
||||
message:
|
||||
errorData?.message ||
|
||||
response?.statusText ||
|
||||
error.message ||
|
||||
'Something went wrong',
|
||||
url: errorData?.url ?? '',
|
||||
errors: (errorData?.errors ?? []).map((e) => ({
|
||||
message: e.message ?? '',
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { TreemapViewType } from 'container/MetricsExplorer/Summary/types';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export interface MetricsTreeMapPayload {
|
||||
filters: TagFilter;
|
||||
limit?: number;
|
||||
treemap?: TreemapViewType;
|
||||
}
|
||||
|
||||
export interface MetricsTreeMapResponse {
|
||||
status: string;
|
||||
data: {
|
||||
[TreemapViewType.TIMESERIES]: TimeseriesData[];
|
||||
[TreemapViewType.SAMPLES]: SamplesData[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface TimeseriesData {
|
||||
percentage: number;
|
||||
total_value: number;
|
||||
metric_name: string;
|
||||
}
|
||||
|
||||
export interface SamplesData {
|
||||
percentage: number;
|
||||
metric_name: string;
|
||||
}
|
||||
|
||||
export const getMetricsTreeMap = async (
|
||||
props: MetricsTreeMapPayload,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponse<MetricsTreeMapResponse> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/metrics/treemap', props, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
params: props,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
import { Temporality } from './getMetricDetails';
|
||||
import { MetricType } from './getMetricsList';
|
||||
|
||||
export interface UpdateMetricMetadataProps {
|
||||
description: string;
|
||||
metricType: MetricType;
|
||||
temporality?: Temporality;
|
||||
isMonotonic?: boolean;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface UpdateMetricMetadataResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const updateMetricMetadata = async (
|
||||
metricName: string,
|
||||
props: UpdateMetricMetadataProps,
|
||||
): Promise<SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(`/metrics/${metricName}/metadata`, {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateMetricMetadata;
|
||||
@@ -49,7 +49,6 @@ export const REACT_QUERY_KEY = {
|
||||
|
||||
// Metrics Explorer Query Keys
|
||||
GET_METRICS_LIST: 'GET_METRICS_LIST',
|
||||
GET_METRICS_TREE_MAP: 'GET_METRICS_TREE_MAP',
|
||||
GET_METRICS_LIST_FILTER_KEYS: 'GET_METRICS_LIST_FILTER_KEYS',
|
||||
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
|
||||
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',
|
||||
|
||||
@@ -3,16 +3,14 @@ import { UseQueryResult } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Card, Skeleton, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { useGetGraphCustomSeries } from 'components/CeleryTask/useGetGraphCustomSeries';
|
||||
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
getCustomFiltersForBarChart,
|
||||
getFormattedEndPointStatusCodeChartData,
|
||||
getStatusCodeBarChartWidgetData,
|
||||
statusCodeWidgetInfo,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
@@ -20,15 +18,16 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { Options } from 'uplot';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
import { prepareStatusCodeBarChartsConfig } from './utils';
|
||||
|
||||
function StatusCodeBarCharts({
|
||||
endPointStatusCodeBarChartsDataQuery,
|
||||
@@ -67,13 +66,6 @@ function StatusCodeBarCharts({
|
||||
} = endPointStatusCodeLatencyBarChartsDataQuery;
|
||||
|
||||
const { startTime: minTime, endTime: maxTime } = timeRange;
|
||||
const legendScrollPositionRef = useRef<{
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}>({
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
});
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const dimensions = useResizeObserver(graphRef);
|
||||
@@ -119,6 +111,7 @@ function StatusCodeBarCharts({
|
||||
|
||||
const navigateToExplorer = useNavigateToExplorer();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
const navigateToExplorerPages = useNavigateToExplorerPages();
|
||||
const { notifications } = useNotifications();
|
||||
@@ -134,12 +127,6 @@ function StatusCodeBarCharts({
|
||||
[],
|
||||
);
|
||||
|
||||
const { getCustomSeries } = useGetGraphCustomSeries({
|
||||
isDarkMode,
|
||||
drawStyle: 'bars',
|
||||
colorMapping,
|
||||
});
|
||||
|
||||
const widget = useMemo<Widgets>(
|
||||
() =>
|
||||
getStatusCodeBarChartWidgetData(domainName, {
|
||||
@@ -193,49 +180,36 @@ function StatusCodeBarCharts({
|
||||
],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
apiResponse:
|
||||
currentWidgetInfoIndex === 0
|
||||
? formattedEndPointStatusCodeBarChartsDataPayload
|
||||
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||
isDarkMode,
|
||||
dimensions,
|
||||
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
minTimeScale: minTime,
|
||||
maxTimeScale: maxTime,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
onClickHandler: graphClickHandler,
|
||||
customSeries: getCustomSeries,
|
||||
onDragSelect,
|
||||
colorMapping,
|
||||
query: currentQuery,
|
||||
legendScrollPosition: legendScrollPositionRef.current,
|
||||
setLegendScrollPosition: (position: {
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}) => {
|
||||
legendScrollPositionRef.current = position;
|
||||
},
|
||||
}),
|
||||
[
|
||||
minTime,
|
||||
maxTime,
|
||||
currentWidgetInfoIndex,
|
||||
dimensions,
|
||||
formattedEndPointStatusCodeBarChartsDataPayload,
|
||||
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||
const config = useMemo(() => {
|
||||
const apiResponse =
|
||||
currentWidgetInfoIndex === 0
|
||||
? formattedEndPointStatusCodeBarChartsDataPayload
|
||||
: formattedEndPointStatusCodeLatencyBarChartsDataPayload;
|
||||
return prepareStatusCodeBarChartsConfig({
|
||||
timezone,
|
||||
isDarkMode,
|
||||
graphClickHandler,
|
||||
getCustomSeries,
|
||||
query: currentQuery,
|
||||
onDragSelect,
|
||||
onClick: graphClickHandler,
|
||||
apiResponse,
|
||||
minTimeScale: minTime,
|
||||
maxTimeScale: maxTime,
|
||||
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
|
||||
colorMapping,
|
||||
currentQuery,
|
||||
],
|
||||
);
|
||||
});
|
||||
}, [
|
||||
currentQuery,
|
||||
isDarkMode,
|
||||
minTime,
|
||||
maxTime,
|
||||
graphClickHandler,
|
||||
onDragSelect,
|
||||
formattedEndPointStatusCodeBarChartsDataPayload,
|
||||
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||
timezone,
|
||||
currentWidgetInfoIndex,
|
||||
colorMapping,
|
||||
]);
|
||||
|
||||
const renderCardContent = useCallback(
|
||||
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
|
||||
@@ -253,11 +227,20 @@ function StatusCodeBarCharts({
|
||||
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||
})}
|
||||
>
|
||||
<Uplot options={options as Options} data={chartData} />
|
||||
<BarChart
|
||||
config={config}
|
||||
data={chartData}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
timezone={timezone}
|
||||
legendConfig={{
|
||||
position: LegendPosition.BOTTOM,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[options, chartData],
|
||||
[config, chartData, dimensions, timezone],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { get } from 'lodash-es';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryData } from 'types/api/widgets/getQuery';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export const prepareStatusCodeBarChartsConfig = ({
|
||||
timezone,
|
||||
isDarkMode,
|
||||
query,
|
||||
onDragSelect,
|
||||
onClick,
|
||||
apiResponse,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
yAxisUnit,
|
||||
colorMapping,
|
||||
}: {
|
||||
timezone: Timezone;
|
||||
isDarkMode: boolean;
|
||||
query: Query;
|
||||
onDragSelect: (startTime: number, endTime: number) => void;
|
||||
onClick?: OnClickPluginOpts['onClick'];
|
||||
minTimeScale?: number;
|
||||
maxTimeScale?: number;
|
||||
apiResponse: MetricRangePayloadProps;
|
||||
yAxisUnit?: string;
|
||||
colorMapping?: Record<string, string>;
|
||||
}): UPlotConfigBuilder => {
|
||||
const stepIntervals: ExecStats['stepIntervals'] = get(
|
||||
apiResponse,
|
||||
'data.newResult.meta.stepIntervals',
|
||||
{},
|
||||
);
|
||||
const minStepInterval = Math.min(...Object.values(stepIntervals));
|
||||
|
||||
const config = buildBaseConfig({
|
||||
id: uuid(),
|
||||
yAxisUnit: yAxisUnit,
|
||||
apiResponse,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
timezone,
|
||||
onClick,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
stepInterval: minStepInterval,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
});
|
||||
|
||||
const seriesList: QueryData[] = apiResponse?.data?.result || [];
|
||||
seriesList.forEach((series) => {
|
||||
const baseLabelName = getLabelName(
|
||||
series.metric,
|
||||
series.queryName || '', // query
|
||||
series.legend || '',
|
||||
);
|
||||
|
||||
const label = query ? getLegend(series, query, baseLabelName) : baseLabelName;
|
||||
|
||||
const currentStepInterval = get(stepIntervals, series.queryName, undefined);
|
||||
|
||||
config.addSeries({
|
||||
scaleKey: 'y',
|
||||
drawStyle: DrawStyle.Bar,
|
||||
label: label,
|
||||
colorMapping: colorMapping ?? {},
|
||||
isDarkMode,
|
||||
stepInterval: currentStepInterval,
|
||||
});
|
||||
});
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -21,10 +21,15 @@ interface MockQueryResult {
|
||||
}
|
||||
|
||||
// Mocks
|
||||
jest.mock('components/Uplot', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => <div data-testid="uplot-mock" />),
|
||||
}));
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/BarChart/BarChart',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('components/CeleryTask/useGetGraphCustomSeries', () => ({
|
||||
useGetGraphCustomSeries: (): { getCustomSeries: jest.Mock } => ({
|
||||
@@ -70,6 +75,24 @@ jest.mock('hooks/useNotifications', () => ({
|
||||
useNotifications: (): { notifications: [] } => ({ notifications: [] }),
|
||||
}));
|
||||
|
||||
jest.mock('providers/Timezone', () => ({
|
||||
useTimezone: (): {
|
||||
timezone: {
|
||||
name: string;
|
||||
value: string;
|
||||
offset: string;
|
||||
searchIndex: string;
|
||||
};
|
||||
} => ({
|
||||
timezone: {
|
||||
name: 'UTC',
|
||||
value: 'UTC',
|
||||
offset: '+00:00',
|
||||
searchIndex: 'UTC',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('lib/uPlotLib/getUplotChartOptions', () => ({
|
||||
getUPlotChartOptions: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
@@ -319,7 +342,7 @@ describe('StatusCodeBarCharts', () => {
|
||||
mockData.payload,
|
||||
'sum',
|
||||
);
|
||||
expect(screen.getByTestId('uplot-mock')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('bar-chart-mock')).toBeInTheDocument();
|
||||
expect(screen.getByText('Number of calls')).toBeInTheDocument();
|
||||
expect(screen.getByText('Latency')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -179,19 +179,6 @@
|
||||
&__input.description {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
color: var(--text-ink-400);
|
||||
background: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--text-ink-500);
|
||||
background: var(--bg-vanilla-200);
|
||||
border-color: var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-alert-header {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const getRandomColor = (): string => {
|
||||
};
|
||||
|
||||
export const DATASOURCE_VS_ROUTES: Record<DataSource, string> = {
|
||||
[DataSource.METRICS]: ROUTES.METRICS_EXPLORER,
|
||||
[DataSource.METRICS]: ROUTES.METRICS_EXPLORER_EXPLORER,
|
||||
[DataSource.TRACES]: ROUTES.TRACES_EXPLORER,
|
||||
[DataSource.LOGS]: ROUTES.LOGS_EXPLORER,
|
||||
};
|
||||
|
||||
@@ -12,14 +12,21 @@ import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interface
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import {
|
||||
ICurrentQueryData,
|
||||
useHandleExplorerTabChange,
|
||||
} from 'hooks/useHandleExplorerTabChange';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { Warning } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { MetricAggregation } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { explorerViewToPanelType } from 'utils/explorerUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
@@ -42,15 +49,20 @@ function Explorer(): JSX.Element {
|
||||
stagedQuery,
|
||||
updateAllQueriesOperators,
|
||||
currentQuery,
|
||||
handleSetConfig,
|
||||
} = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(false);
|
||||
|
||||
const metricNames = useMemo(() => {
|
||||
const currentMetricNames: string[] = [];
|
||||
stagedQuery?.builder.queryData.forEach((query) => {
|
||||
if (query.aggregateAttribute?.key) {
|
||||
currentMetricNames.push(query.aggregateAttribute?.key);
|
||||
const metricName =
|
||||
query.aggregateAttribute?.key ||
|
||||
(query.aggregations?.[0] as MetricAggregation | undefined)?.metricName;
|
||||
if (metricName) {
|
||||
currentMetricNames.push(metricName);
|
||||
}
|
||||
});
|
||||
return currentMetricNames;
|
||||
@@ -176,6 +188,16 @@ function Explorer(): JSX.Element {
|
||||
|
||||
useShareBuilderUrl({ defaultValue: defaultQuery });
|
||||
|
||||
const handleChangeSelectedView = useCallback(
|
||||
(view: ExplorerViews, querySearchParameters?: ICurrentQueryData): void => {
|
||||
const nextPanelType =
|
||||
explorerViewToPanelType[view] || PANEL_TYPES.TIME_SERIES;
|
||||
handleSetConfig(nextPanelType, DataSource.METRICS);
|
||||
handleExplorerTabChange(nextPanelType, querySearchParameters);
|
||||
},
|
||||
[handleSetConfig, handleExplorerTabChange],
|
||||
);
|
||||
|
||||
const handleExport = useCallback(
|
||||
(
|
||||
dashboard: Dashboard | null,
|
||||
@@ -348,6 +370,7 @@ function Explorer(): JSX.Element {
|
||||
onExport={handleExport}
|
||||
isOneChartPerQuery={showOneChartPerQuery}
|
||||
splitedQueries={splitedQueries}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
{isMetricDetailsOpen && selectedMetricName && (
|
||||
<MetricDetails
|
||||
|
||||
@@ -12,6 +12,7 @@ import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import * as useOptionsMenuHooks from 'container/OptionsMenu';
|
||||
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
|
||||
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import * as useHandleExplorerTabChangeHooks from 'hooks/useHandleExplorerTabChange';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||
import * as timezoneHooks from 'providers/Timezone';
|
||||
@@ -29,6 +30,8 @@ const queryClient = new QueryClient();
|
||||
const mockUpdateAllQueriesOperators = jest
|
||||
.fn()
|
||||
.mockReturnValue(initialQueriesMap[DataSource.METRICS]);
|
||||
const mockHandleSetConfig = jest.fn();
|
||||
const mockHandleExplorerTabChange = jest.fn();
|
||||
const mockUseQueryBuilderData = {
|
||||
handleRunQuery: jest.fn(),
|
||||
stagedQuery: initialQueriesMap[DataSource.METRICS],
|
||||
@@ -40,7 +43,7 @@ const mockUseQueryBuilderData = {
|
||||
handleSetQueryData: jest.fn(),
|
||||
handleSetFormulaData: jest.fn(),
|
||||
handleSetQueryItemData: jest.fn(),
|
||||
handleSetConfig: jest.fn(),
|
||||
handleSetConfig: mockHandleSetConfig,
|
||||
removeQueryBuilderEntityByIndex: jest.fn(),
|
||||
removeQueryTypeItemByIndex: jest.fn(),
|
||||
isDefaultQuery: jest.fn(),
|
||||
@@ -135,6 +138,11 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
|
||||
...mockUseQueryBuilderData,
|
||||
} as any);
|
||||
jest
|
||||
.spyOn(useHandleExplorerTabChangeHooks, 'useHandleExplorerTabChange')
|
||||
.mockReturnValue({
|
||||
handleExplorerTabChange: mockHandleExplorerTabChange,
|
||||
});
|
||||
|
||||
const Y_AXIS_UNIT_SELECTOR_TEST_ID = 'y-axis-unit-selector';
|
||||
|
||||
@@ -382,4 +390,109 @@ describe('Explorer', () => {
|
||||
expect(oneChartPerQueryToggle).toBeEnabled();
|
||||
expect(oneChartPerQueryToggle).not.toBeChecked();
|
||||
});
|
||||
|
||||
describe('loading saved views with v5 query format', () => {
|
||||
const EMPTY_STATE_TEXT = 'Select a metric and run a query to see the results';
|
||||
|
||||
it('should show empty state when no metric is selected', () => {
|
||||
(useSearchParams as jest.Mock).mockReturnValue([
|
||||
new URLSearchParams({}),
|
||||
mockSetSearchParams,
|
||||
]);
|
||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [],
|
||||
});
|
||||
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
|
||||
...mockUseQueryBuilderData,
|
||||
} as any);
|
||||
|
||||
renderExplorer();
|
||||
|
||||
expect(screen.getByText(EMPTY_STATE_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show empty state when saved view has v5 aggregations format', () => {
|
||||
(useSearchParams as jest.Mock).mockReturnValue([
|
||||
new URLSearchParams({}),
|
||||
mockSetSearchParams,
|
||||
]);
|
||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [MOCK_METRIC_METADATA],
|
||||
});
|
||||
|
||||
// saved view loaded back from v5 format
|
||||
// aggregateAttribute.key is empty (lost in v3/v4 -> v5 -> v3/v4 round trip)
|
||||
// but aggregations[0].metricName has metric name
|
||||
// TODO(srikanthccv): remove this mess
|
||||
const mockQueryData = {
|
||||
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
|
||||
aggregateAttribute: {
|
||||
...(initialQueriesMap[DataSource.METRICS].builder.queryData[0]
|
||||
.aggregateAttribute as BaseAutocompleteData),
|
||||
key: '',
|
||||
},
|
||||
aggregations: [
|
||||
{
|
||||
metricName: 'http_requests_total',
|
||||
temporality: 'cumulative',
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
},
|
||||
],
|
||||
};
|
||||
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue(({
|
||||
...mockUseQueryBuilderData,
|
||||
stagedQuery: {
|
||||
...initialQueriesMap[DataSource.METRICS],
|
||||
builder: {
|
||||
...initialQueriesMap[DataSource.METRICS].builder,
|
||||
queryData: [mockQueryData],
|
||||
},
|
||||
},
|
||||
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
|
||||
|
||||
renderExplorer();
|
||||
|
||||
expect(screen.queryByText(EMPTY_STATE_TEXT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show empty state when query uses v3 aggregateAttribute format', () => {
|
||||
(useSearchParams as jest.Mock).mockReturnValue([
|
||||
new URLSearchParams({}),
|
||||
mockSetSearchParams,
|
||||
]);
|
||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [MOCK_METRIC_METADATA],
|
||||
});
|
||||
|
||||
const mockQueryData = {
|
||||
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
|
||||
aggregateAttribute: {
|
||||
...(initialQueriesMap[DataSource.METRICS].builder.queryData[0]
|
||||
.aggregateAttribute as BaseAutocompleteData),
|
||||
key: 'system_cpu_usage',
|
||||
},
|
||||
};
|
||||
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue(({
|
||||
...mockUseQueryBuilderData,
|
||||
stagedQuery: {
|
||||
...initialQueriesMap[DataSource.METRICS],
|
||||
builder: {
|
||||
...initialQueriesMap[DataSource.METRICS].builder,
|
||||
queryData: [mockQueryData],
|
||||
},
|
||||
},
|
||||
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
|
||||
|
||||
renderExplorer();
|
||||
|
||||
expect(screen.queryByText(EMPTY_STATE_TEXT)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from 'api/generated/services/metrics';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Bell, Grid } from 'lucide-react';
|
||||
import { pluralize } from 'utils/pluralize';
|
||||
|
||||
@@ -18,8 +17,6 @@ import { DashboardsAndAlertsPopoverProps } from './types';
|
||||
function DashboardsAndAlertsPopover({
|
||||
metricName,
|
||||
}: DashboardsAndAlertsPopoverProps): JSX.Element | null {
|
||||
const params = useUrlQuery();
|
||||
|
||||
const {
|
||||
data: alertsData,
|
||||
isLoading: isLoadingAlerts,
|
||||
@@ -71,8 +68,10 @@ function DashboardsAndAlertsPopover({
|
||||
<Typography.Link
|
||||
key={alert.alertId}
|
||||
onClick={(): void => {
|
||||
params.set(QueryParams.ruleId, alert.alertId);
|
||||
window.open(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`, '_blank');
|
||||
window.open(
|
||||
`${ROUTES.ALERT_OVERVIEW}?${QueryParams.ruleId}=${alert.alertId}`,
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
>
|
||||
@@ -82,7 +81,7 @@ function DashboardsAndAlertsPopover({
|
||||
}));
|
||||
}
|
||||
return null;
|
||||
}, [alerts, params]);
|
||||
}, [alerts]);
|
||||
|
||||
const dashboardsPopoverContent = useMemo(() => {
|
||||
if (dashboards && dashboards.length > 0) {
|
||||
|
||||
@@ -16,15 +16,6 @@ import {
|
||||
|
||||
const mockWindowOpen = jest.fn();
|
||||
Object.defineProperty(window, 'open', { value: mockWindowOpen });
|
||||
const mockSetQuery = jest.fn();
|
||||
const mockUrlQuery = {
|
||||
set: mockSetQuery,
|
||||
toString: jest.fn(),
|
||||
};
|
||||
jest.mock('hooks/useUrlQuery', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => mockUrlQuery),
|
||||
}));
|
||||
|
||||
const useGetMetricAlertsMock = jest.spyOn(
|
||||
metricsExplorerHooks,
|
||||
@@ -156,12 +147,10 @@ describe('DashboardsAndAlertsPopover', () => {
|
||||
// Click on the first alert rule
|
||||
await userEvent.click(screen.getByText(MOCK_ALERT_1.alertName));
|
||||
|
||||
// Should open alert in new tab
|
||||
expect(mockSetQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleId,
|
||||
MOCK_ALERT_1.alertId,
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
`/alerts/overview?${QueryParams.ruleId}=${MOCK_ALERT_1.alertId}`,
|
||||
'_blank',
|
||||
);
|
||||
expect(mockWindowOpen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders unique dashboards even when there are duplicates', async () => {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Typography } from 'antd';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import {
|
||||
BarChart,
|
||||
BarChart2,
|
||||
BarChartHorizontal,
|
||||
Diff,
|
||||
Gauge,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { METRIC_TYPE_LABEL_MAP } from './constants';
|
||||
|
||||
// TODO: @amlannandy Delete this component after API migration is complete
|
||||
function MetricTypeRenderer({ type }: { type: MetricType }): JSX.Element {
|
||||
const [icon, color] = useMemo(() => {
|
||||
switch (type) {
|
||||
case MetricType.SUM:
|
||||
return [
|
||||
<Diff key={type} size={12} color={Color.BG_ROBIN_500} />,
|
||||
Color.BG_ROBIN_500,
|
||||
];
|
||||
case MetricType.GAUGE:
|
||||
return [
|
||||
<Gauge key={type} size={12} color={Color.BG_SAKURA_500} />,
|
||||
Color.BG_SAKURA_500,
|
||||
];
|
||||
case MetricType.HISTOGRAM:
|
||||
return [
|
||||
<BarChart2 key={type} size={12} color={Color.BG_SIENNA_500} />,
|
||||
Color.BG_SIENNA_500,
|
||||
];
|
||||
case MetricType.SUMMARY:
|
||||
return [
|
||||
<BarChartHorizontal key={type} size={12} color={Color.BG_FOREST_500} />,
|
||||
Color.BG_FOREST_500,
|
||||
];
|
||||
case MetricType.EXPONENTIAL_HISTOGRAM:
|
||||
return [
|
||||
<BarChart key={type} size={12} color={Color.BG_AQUA_500} />,
|
||||
Color.BG_AQUA_500,
|
||||
];
|
||||
default:
|
||||
return [null, ''];
|
||||
}
|
||||
}, [type]);
|
||||
|
||||
const metricTypeRendererStyle = useMemo(
|
||||
() => ({
|
||||
backgroundColor: `${color}33`,
|
||||
border: `1px solid ${color}`,
|
||||
color,
|
||||
}),
|
||||
[color],
|
||||
);
|
||||
|
||||
const metricTypeRendererTextStyle = useMemo(
|
||||
() => ({
|
||||
color,
|
||||
fontSize: 12,
|
||||
}),
|
||||
[color],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="metric-type-renderer" style={metricTypeRendererStyle}>
|
||||
{icon}
|
||||
<Typography.Text style={metricTypeRendererTextStyle}>
|
||||
{METRIC_TYPE_LABEL_MAP[type]}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricTypeRenderer;
|
||||
@@ -47,7 +47,7 @@ function MetricsSearch({
|
||||
}}
|
||||
onRun={handleRunQuery}
|
||||
showFilterSuggestionsWithoutMetric
|
||||
placeholder="Try metric_name CONTAINS 'http.server' to view all HTTP Server metrics being sent"
|
||||
placeholder="Search your metrics. Try service.name='api' to see all API service metrics, or http.client for HTTP client metrics."
|
||||
/>
|
||||
</div>
|
||||
<RunQueryBtn
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'antd';
|
||||
import { SorterResult } from 'antd/es/table/interface';
|
||||
import { Querybuildertypesv5OrderDirectionDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
import { MetricsListItemRowData, MetricsTableProps } from './types';
|
||||
@@ -18,6 +19,7 @@ import { getMetricsTableColumns } from './utils';
|
||||
function MetricsTable({
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
data,
|
||||
pageSize,
|
||||
currentPage,
|
||||
@@ -71,54 +73,54 @@ function MetricsTable({
|
||||
<Info size={16} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Table
|
||||
loading={{
|
||||
spinning: isLoading,
|
||||
indicator: (
|
||||
<Spin
|
||||
data-testid="metrics-table-loading-state"
|
||||
indicator={<LoadingOutlined size={14} spin />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
dataSource={data}
|
||||
columns={getMetricsTableColumns(queryFilterExpression, onFilterChange)}
|
||||
locale={{
|
||||
emptyText: isLoading ? null : (
|
||||
<div
|
||||
className="no-metrics-message-container"
|
||||
data-testid={
|
||||
isError ? 'metrics-table-error-state' : 'metrics-table-empty-state'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
{isError && error ? (
|
||||
<ErrorInPlace error={error} />
|
||||
) : (
|
||||
<Table
|
||||
loading={{
|
||||
spinning: isLoading,
|
||||
indicator: (
|
||||
<Spin
|
||||
data-testid="metrics-table-loading-state"
|
||||
indicator={<LoadingOutlined size={14} spin />}
|
||||
/>
|
||||
<Typography.Text className="no-metrics-message">
|
||||
{isError
|
||||
? 'Error fetching metrics. If the problem persists, please contact support.'
|
||||
: 'This query had no results. Edit your query and try again!'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
total: totalCount,
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => openMetricDetails(record.key, 'list'),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
dataSource={data}
|
||||
columns={getMetricsTableColumns(queryFilterExpression, onFilterChange)}
|
||||
locale={{
|
||||
emptyText: isLoading ? null : (
|
||||
<div
|
||||
className="no-metrics-message-container"
|
||||
data-testid="metrics-table-empty-state"
|
||||
>
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
<Typography.Text className="no-metrics-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
tableLayout="fixed"
|
||||
onChange={handleTableChange}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
showSizeChanger: true,
|
||||
hideOnSinglePage: false,
|
||||
onChange: onPaginationChange,
|
||||
total: totalCount,
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => openMetricDetails(record.key, 'list'),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Group } from '@visx/group';
|
||||
import { Treemap } from '@visx/hierarchy';
|
||||
import { Empty, Select, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import { MetricsexplorertypesTreemapModeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
|
||||
import { HierarchyNode, stratify, treemapBinary } from 'd3-hierarchy';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
function MetricsTreemapInternal({
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
data,
|
||||
viewType,
|
||||
openMetricDetails,
|
||||
@@ -91,6 +93,10 @@ function MetricsTreemapInternal({
|
||||
);
|
||||
}
|
||||
|
||||
if (isError && error) {
|
||||
return <ErrorInPlace error={error} />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Empty
|
||||
@@ -174,6 +180,7 @@ function MetricsTreemap({
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
openMetricDetails,
|
||||
setHeatmapView,
|
||||
}: MetricsTreemapProps): JSX.Element {
|
||||
@@ -202,6 +209,7 @@ function MetricsTreemap({
|
||||
<MetricsTreemapInternal
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
error={error}
|
||||
data={data}
|
||||
viewType={viewType}
|
||||
openMetricDetails={openMetricDetails}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
useGetMetricsStats,
|
||||
useGetMetricsTreemap,
|
||||
@@ -63,13 +64,20 @@ function Summary(): JSX.Element {
|
||||
MetricsexplorertypesTreemapModeDTO.samples,
|
||||
);
|
||||
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const {
|
||||
currentQuery,
|
||||
stagedQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
useShareBuilderUrl({ defaultValue: initialQueriesMap[DataSource.METRICS] });
|
||||
|
||||
const query = useMemo(() => currentQuery?.builder?.queryData[0], [
|
||||
currentQuery,
|
||||
]);
|
||||
const query = useMemo(
|
||||
() =>
|
||||
stagedQuery?.builder?.queryData?.[0] ||
|
||||
initialQueriesMap[DataSource.METRICS].builder.queryData[0],
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(
|
||||
@@ -86,14 +94,16 @@ function Summary(): JSX.Element {
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const appliedFilterExpression = query?.filter?.expression || '';
|
||||
|
||||
const [
|
||||
currentQueryFilterExpression,
|
||||
setCurrentQueryFilterExpression,
|
||||
] = useState<string>(query?.filter?.expression || '');
|
||||
] = useState<string>(appliedFilterExpression);
|
||||
|
||||
const [appliedFilterExpression, setAppliedFilterExpression] = useState(
|
||||
query?.filter?.expression || '',
|
||||
);
|
||||
useEffect(() => {
|
||||
setCurrentQueryFilterExpression(appliedFilterExpression);
|
||||
}, [appliedFilterExpression]);
|
||||
|
||||
const queryFilterExpression = useMemo(
|
||||
() => ({ expression: appliedFilterExpression }),
|
||||
@@ -150,6 +160,7 @@ function Summary(): JSX.Element {
|
||||
mutate: getMetricsStats,
|
||||
isLoading: isGetMetricsStatsLoading,
|
||||
isError: isGetMetricsStatsError,
|
||||
error: metricsStatsError,
|
||||
} = useGetMetricsStats();
|
||||
|
||||
const {
|
||||
@@ -157,8 +168,19 @@ function Summary(): JSX.Element {
|
||||
mutate: getMetricsTreemap,
|
||||
isLoading: isGetMetricsTreemapLoading,
|
||||
isError: isGetMetricsTreemapError,
|
||||
error: metricsTreemapError,
|
||||
} = useGetMetricsTreemap();
|
||||
|
||||
const metricsStatsApiError = useMemo(
|
||||
() => convertToApiError(metricsStatsError),
|
||||
[metricsStatsError],
|
||||
);
|
||||
|
||||
const metricsTreemapApiError = useMemo(
|
||||
() => convertToApiError(metricsTreemapError),
|
||||
[metricsTreemapError],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getMetricsStats({
|
||||
data: metricsListQuery,
|
||||
@@ -192,8 +214,6 @@ function Summary(): JSX.Element {
|
||||
],
|
||||
},
|
||||
});
|
||||
setCurrentQueryFilterExpression(expression);
|
||||
setAppliedFilterExpression(expression);
|
||||
setCurrentPage(1);
|
||||
if (expression) {
|
||||
logEvent(MetricsExplorerEvents.FilterApplied, {
|
||||
@@ -290,10 +310,14 @@ function Summary(): JSX.Element {
|
||||
};
|
||||
|
||||
const isMetricsListDataEmpty =
|
||||
formattedMetricsData.length === 0 && !isGetMetricsStatsLoading;
|
||||
formattedMetricsData.length === 0 &&
|
||||
!isGetMetricsStatsLoading &&
|
||||
!isGetMetricsStatsError;
|
||||
|
||||
const isMetricsTreeMapDataEmpty =
|
||||
!treeMapData?.data[heatmapView]?.length && !isGetMetricsTreemapLoading;
|
||||
!treeMapData?.data[heatmapView]?.length &&
|
||||
!isGetMetricsTreemapLoading &&
|
||||
!isGetMetricsTreemapError;
|
||||
|
||||
const showFullScreenLoading =
|
||||
(isGetMetricsStatsLoading || isGetMetricsTreemapLoading) &&
|
||||
@@ -322,6 +346,7 @@ function Summary(): JSX.Element {
|
||||
data={treeMapData?.data}
|
||||
isLoading={isGetMetricsTreemapLoading}
|
||||
isError={isGetMetricsTreemapError}
|
||||
error={metricsTreemapApiError}
|
||||
viewType={heatmapView}
|
||||
openMetricDetails={openMetricDetails}
|
||||
setHeatmapView={handleSetHeatmapView}
|
||||
@@ -329,6 +354,7 @@ function Summary(): JSX.Element {
|
||||
<MetricsTable
|
||||
isLoading={isGetMetricsStatsLoading}
|
||||
isError={isGetMetricsStatsError}
|
||||
error={metricsStatsApiError}
|
||||
data={formattedMetricsData}
|
||||
pageSize={pageSize}
|
||||
currentPage={currentPage}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
|
||||
import MetricTypeRenderer from '../MetricTypeRenderer';
|
||||
|
||||
jest.mock('lucide-react', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
Diff: (): JSX.Element => <svg data-testid="diff-icon" />,
|
||||
Gauge: (): JSX.Element => <svg data-testid="gauge-icon" />,
|
||||
BarChart2: (): JSX.Element => <svg data-testid="bar-chart-2-icon" />,
|
||||
BarChartHorizontal: (): JSX.Element => (
|
||||
<svg data-testid="bar-chart-horizontal-icon" />
|
||||
),
|
||||
BarChart: (): JSX.Element => <svg data-testid="bar-chart-icon" />,
|
||||
};
|
||||
});
|
||||
|
||||
describe('MetricTypeRenderer', () => {
|
||||
it('should render correct icon and color for each metric type', () => {
|
||||
const types = [
|
||||
{
|
||||
type: MetricType.SUM,
|
||||
color: Color.BG_ROBIN_500,
|
||||
iconTestId: 'diff-icon',
|
||||
},
|
||||
{
|
||||
type: MetricType.GAUGE,
|
||||
color: Color.BG_SAKURA_500,
|
||||
iconTestId: 'gauge-icon',
|
||||
},
|
||||
{
|
||||
type: MetricType.HISTOGRAM,
|
||||
color: Color.BG_SIENNA_500,
|
||||
iconTestId: 'bar-chart-2-icon',
|
||||
},
|
||||
{
|
||||
type: MetricType.SUMMARY,
|
||||
color: Color.BG_FOREST_500,
|
||||
iconTestId: 'bar-chart-horizontal-icon',
|
||||
},
|
||||
{
|
||||
type: MetricType.EXPONENTIAL_HISTOGRAM,
|
||||
color: Color.BG_AQUA_500,
|
||||
iconTestId: 'bar-chart-icon',
|
||||
},
|
||||
];
|
||||
|
||||
types.forEach(({ type, color, iconTestId }) => {
|
||||
const { container } = render(<MetricTypeRenderer type={type} />);
|
||||
const rendererDiv = container.firstChild as HTMLElement;
|
||||
|
||||
expect(rendererDiv).toHaveStyle({
|
||||
backgroundColor: `${color}33`,
|
||||
border: `1px solid ${color}`,
|
||||
color,
|
||||
});
|
||||
|
||||
expect(screen.getByTestId(iconTestId)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { Filter } from 'api/v5/v5';
|
||||
import * as useGetMetricsListFilterValues from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
|
||||
import * as useQueryBuilderOperationsHooks from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import store from 'store';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import MetricsTable from '../MetricsTable';
|
||||
import { MetricsListItemRowData } from '../types';
|
||||
@@ -119,12 +120,23 @@ describe('MetricsTable', () => {
|
||||
});
|
||||
|
||||
it('shows error state', () => {
|
||||
const mockError = new APIError({
|
||||
httpStatusCode: 400,
|
||||
error: {
|
||||
code: '400',
|
||||
message: 'invalid filter expression',
|
||||
url: '',
|
||||
errors: [],
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTable
|
||||
isLoading={false}
|
||||
isError
|
||||
error={mockError}
|
||||
data={[]}
|
||||
pageSize={10}
|
||||
currentPage={1}
|
||||
@@ -139,12 +151,8 @@ describe('MetricsTable', () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('metrics-table-error-state')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Error fetching metrics. If the problem persists, please contact support.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('400')).toBeInTheDocument();
|
||||
expect(screen.getByText('invalid filter expression')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty state when no data', () => {
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import * as metricsHooks from 'api/generated/services/metrics';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import * as useGetMetricsListHooks from 'hooks/metricsExplorer/useGetMetricsList';
|
||||
import * as useGetMetricsTreeMapHooks from 'hooks/metricsExplorer/useGetMetricsTreeMap';
|
||||
import store from 'store';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { render, screen, waitFor } from 'tests/test-utils';
|
||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
|
||||
import Summary from '../Summary';
|
||||
import { TreemapViewType } from '../types';
|
||||
|
||||
jest.mock('d3-hierarchy', () => ({
|
||||
stratify: jest.fn().mockReturnValue({
|
||||
@@ -44,58 +40,135 @@ jest.mock('react-router-dom', () => ({
|
||||
pathname: `${ROUTES.METRICS_EXPLORER_BASE}`,
|
||||
}),
|
||||
}));
|
||||
jest.mock('hooks/queryBuilder/useShareBuilderUrl', () => ({
|
||||
useShareBuilderUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
// so filter expression assertions easy
|
||||
jest.mock('../MetricsSearch', () => {
|
||||
return function MockMetricsSearch(props: {
|
||||
currentQueryFilterExpression: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div data-testid="metrics-search-expression">
|
||||
{props.currentQueryFilterExpression}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const mockMetricName = 'test-metric';
|
||||
jest.spyOn(useGetMetricsListHooks, 'useGetMetricsList').mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
metrics: [
|
||||
{
|
||||
metric_name: mockMetricName,
|
||||
description: 'description for a test metric',
|
||||
type: MetricType.GAUGE,
|
||||
unit: 'count',
|
||||
lastReceived: '1715702400',
|
||||
[TreemapViewType.TIMESERIES]: 100,
|
||||
[TreemapViewType.SAMPLES]: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
} as any);
|
||||
jest.spyOn(useGetMetricsTreeMapHooks, 'useGetMetricsTreeMap').mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
[TreemapViewType.TIMESERIES]: [
|
||||
{
|
||||
metric_name: mockMetricName,
|
||||
percentage: 100,
|
||||
total_value: 100,
|
||||
},
|
||||
],
|
||||
[TreemapViewType.SAMPLES]: [
|
||||
{
|
||||
metric_name: mockMetricName,
|
||||
percentage: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
} as any);
|
||||
const mockSetSearchParams = jest.fn();
|
||||
const mockGetMetricsStats = jest.fn();
|
||||
const mockGetMetricsTreemap = jest.fn();
|
||||
|
||||
const mockUseQueryBuilderData = {
|
||||
handleRunQuery: jest.fn(),
|
||||
stagedQuery: initialQueriesMap[DataSource.METRICS],
|
||||
updateAllQueriesOperators: jest.fn(),
|
||||
currentQuery: initialQueriesMap[DataSource.METRICS],
|
||||
resetQuery: jest.fn(),
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
isStagedQueryUpdated: jest.fn(),
|
||||
handleSetQueryData: jest.fn(),
|
||||
handleSetFormulaData: jest.fn(),
|
||||
handleSetQueryItemData: jest.fn(),
|
||||
handleSetConfig: jest.fn(),
|
||||
removeQueryBuilderEntityByIndex: jest.fn(),
|
||||
removeQueryTypeItemByIndex: jest.fn(),
|
||||
isDefaultQuery: jest.fn(),
|
||||
};
|
||||
|
||||
const useGetMetricsStatsSpy = jest.spyOn(metricsHooks, 'useGetMetricsStats');
|
||||
const useGetMetricsTreemapSpy = jest.spyOn(
|
||||
metricsHooks,
|
||||
'useGetMetricsTreemap',
|
||||
);
|
||||
const useQueryBuilderSpy = jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder');
|
||||
|
||||
describe('Summary', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
(useSearchParams as jest.Mock).mockReturnValue([
|
||||
new URLSearchParams(),
|
||||
mockSetSearchParams,
|
||||
]);
|
||||
|
||||
useGetMetricsStatsSpy.mockReturnValue({
|
||||
data: null,
|
||||
mutate: mockGetMetricsStats,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
isIdle: true,
|
||||
isSuccess: false,
|
||||
reset: jest.fn(),
|
||||
status: 'idle',
|
||||
} as any);
|
||||
|
||||
useGetMetricsTreemapSpy.mockReturnValue({
|
||||
data: null,
|
||||
mutate: mockGetMetricsTreemap,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
isIdle: true,
|
||||
isSuccess: false,
|
||||
reset: jest.fn(),
|
||||
status: 'idle',
|
||||
} as any);
|
||||
|
||||
useQueryBuilderSpy.mockReturnValue(({
|
||||
...mockUseQueryBuilderData,
|
||||
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
|
||||
});
|
||||
|
||||
it('does not carry filter expression from a previous page', async () => {
|
||||
const staleFilterExpression = "service.name = 'redis'";
|
||||
|
||||
// prev filter from logs explorer
|
||||
const staleQuery = {
|
||||
...initialQueriesMap[DataSource.METRICS],
|
||||
builder: {
|
||||
...initialQueriesMap[DataSource.METRICS].builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
|
||||
filter: { expression: staleFilterExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// stagedQuery has stale filter (before QueryBuilder resets it)
|
||||
useQueryBuilderSpy.mockReturnValue(({
|
||||
...mockUseQueryBuilderData,
|
||||
stagedQuery: staleQuery,
|
||||
currentQuery: staleQuery,
|
||||
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
|
||||
|
||||
const { rerender } = render(<Summary />);
|
||||
|
||||
expect(screen.getByTestId('metrics-search-expression')).toHaveTextContent(
|
||||
staleFilterExpression,
|
||||
);
|
||||
|
||||
// QB route change effect resets stagedQuery to null
|
||||
useQueryBuilderSpy.mockReturnValue(({
|
||||
...mockUseQueryBuilderData,
|
||||
stagedQuery: null,
|
||||
currentQuery: initialQueriesMap[DataSource.METRICS],
|
||||
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
|
||||
|
||||
rerender(<Summary />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId('metrics-search-expression'),
|
||||
).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
it('persists inspect modal open state across page refresh', () => {
|
||||
(useSearchParams as jest.Mock).mockReturnValue([
|
||||
new URLSearchParams({
|
||||
@@ -105,13 +178,7 @@ describe('Summary', () => {
|
||||
mockSetSearchParams,
|
||||
]);
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<Summary />
|
||||
</Provider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
render(<Summary />);
|
||||
|
||||
expect(screen.queryByText('Proportion View')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -120,18 +187,12 @@ describe('Summary', () => {
|
||||
(useSearchParams as jest.Mock).mockReturnValue([
|
||||
new URLSearchParams({
|
||||
isMetricDetailsOpen: 'true',
|
||||
selectedMetricName: mockMetricName,
|
||||
selectedMetricName: 'test-metric',
|
||||
}),
|
||||
mockSetSearchParams,
|
||||
]);
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<Summary />
|
||||
</Provider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
render(<Summary />);
|
||||
|
||||
expect(screen.queryByText('Proportion View')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
MetricsexplorertypesTreemapModeDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
|
||||
export const METRICS_TABLE_PAGE_SIZE = 10;
|
||||
|
||||
@@ -19,15 +18,6 @@ export const TREEMAP_SQUARE_PADDING = 5;
|
||||
|
||||
export const TREEMAP_MARGINS = { TOP: 10, LEFT: 10, RIGHT: 10, BOTTOM: 10 };
|
||||
|
||||
// TODO: Remove this once API migration is complete
|
||||
export const METRIC_TYPE_LABEL_MAP = {
|
||||
[MetricType.SUM]: 'Sum',
|
||||
[MetricType.GAUGE]: 'Gauge',
|
||||
[MetricType.HISTOGRAM]: 'Histogram',
|
||||
[MetricType.SUMMARY]: 'Summary',
|
||||
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
|
||||
};
|
||||
|
||||
export const METRIC_TYPE_VIEW_LABEL_MAP: Record<MetrictypesTypeDTO, string> = {
|
||||
[MetrictypesTypeDTO.sum]: 'Sum',
|
||||
[MetrictypesTypeDTO.gauge]: 'Gauge',
|
||||
@@ -36,15 +26,6 @@ export const METRIC_TYPE_VIEW_LABEL_MAP: Record<MetrictypesTypeDTO, string> = {
|
||||
[MetrictypesTypeDTO.exponentialhistogram]: 'Exp. Histogram',
|
||||
};
|
||||
|
||||
// TODO(@amlannandy): To remove this once API migration is complete
|
||||
export const METRIC_TYPE_VALUES_MAP: Record<MetricType, string> = {
|
||||
[MetricType.SUM]: 'Sum',
|
||||
[MetricType.GAUGE]: 'Gauge',
|
||||
[MetricType.HISTOGRAM]: 'Histogram',
|
||||
[MetricType.SUMMARY]: 'Summary',
|
||||
[MetricType.EXPONENTIAL_HISTOGRAM]: 'ExponentialHistogram',
|
||||
};
|
||||
|
||||
export const METRIC_TYPE_VIEW_VALUES_MAP: Record<MetrictypesTypeDTO, string> = {
|
||||
[MetrictypesTypeDTO.sum]: 'Sum',
|
||||
[MetrictypesTypeDTO.gauge]: 'Gauge',
|
||||
|
||||
@@ -5,11 +5,13 @@ import {
|
||||
Querybuildertypesv5OrderByDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { Filter } from 'api/v5/v5';
|
||||
import APIError from 'types/api/error';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export interface MetricsTableProps {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
error?: APIError;
|
||||
data: MetricsListItemRowData[];
|
||||
pageSize: number;
|
||||
currentPage: number;
|
||||
@@ -33,6 +35,7 @@ export interface MetricsTreemapProps {
|
||||
data: MetricsexplorertypesTreemapResponseDTO | undefined;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
error?: APIError;
|
||||
viewType: MetricsexplorertypesTreemapModeDTO;
|
||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||
setHeatmapView: (value: MetricsexplorertypesTreemapModeDTO) => void;
|
||||
@@ -41,6 +44,7 @@ export interface MetricsTreemapProps {
|
||||
export interface MetricsTreemapInternalProps {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
error?: APIError;
|
||||
data: MetricsexplorertypesTreemapResponseDTO | undefined;
|
||||
viewType: MetricsexplorertypesTreemapModeDTO;
|
||||
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import {
|
||||
MetricsexplorertypesListMetricDTO,
|
||||
MetrictypesTypeDTO,
|
||||
@@ -221,6 +227,108 @@ describe('MetricNameSelector', () => {
|
||||
|
||||
expect(container.querySelector('.ant-spin-spinning')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('preserves metric search text for signalSource normalization transition (undefined -> empty)', async () => {
|
||||
returnMetrics([makeMetric({ metricName: 'http_requests_total' })]);
|
||||
|
||||
const query = makeQuery({
|
||||
aggregateAttribute: {
|
||||
key: 'http_requests_total',
|
||||
type: '',
|
||||
dataType: DataTypes.Float64,
|
||||
},
|
||||
aggregations: [
|
||||
{
|
||||
metricName: 'http_requests_total',
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
temporality: '',
|
||||
},
|
||||
] as MetricAggregation[],
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<MetricNameSelector
|
||||
query={query}
|
||||
onChange={jest.fn()}
|
||||
signalSource={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<MetricNameSelector query={query} onChange={jest.fn()} signalSource="" />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const lastCall =
|
||||
mockUseListMetrics.mock.calls[mockUseListMetrics.mock.calls.length - 1];
|
||||
expect(lastCall?.[0]).toMatchObject({
|
||||
searchText: 'http_requests_total',
|
||||
limit: 100,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates search text when metric name is hydrated after initial mount', async () => {
|
||||
returnMetrics([makeMetric({ metricName: 'signoz_latency.bucket' })]);
|
||||
|
||||
const emptyQuery = makeQuery({
|
||||
aggregateAttribute: {
|
||||
key: '',
|
||||
type: '',
|
||||
dataType: DataTypes.Float64,
|
||||
},
|
||||
aggregations: [
|
||||
{
|
||||
metricName: '',
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
temporality: '',
|
||||
},
|
||||
] as MetricAggregation[],
|
||||
});
|
||||
|
||||
const hydratedQuery = makeQuery({
|
||||
aggregateAttribute: {
|
||||
key: '',
|
||||
type: '',
|
||||
dataType: DataTypes.Float64,
|
||||
},
|
||||
aggregations: [
|
||||
{
|
||||
metricName: 'signoz_latency.bucket',
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
temporality: '',
|
||||
},
|
||||
] as MetricAggregation[],
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<MetricNameSelector
|
||||
query={emptyQuery}
|
||||
onChange={jest.fn()}
|
||||
signalSource=""
|
||||
/>,
|
||||
);
|
||||
|
||||
rerender(
|
||||
<MetricNameSelector
|
||||
query={hydratedQuery}
|
||||
onChange={jest.fn()}
|
||||
signalSource=""
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const lastCall =
|
||||
mockUseListMetrics.mock.calls[mockUseListMetrics.mock.calls.length - 1];
|
||||
expect(lastCall?.[0]).toMatchObject({
|
||||
searchText: 'signoz_latency.bucket',
|
||||
limit: 100,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('selecting a metric type updates the aggregation options', () => {
|
||||
|
||||
@@ -98,15 +98,30 @@ export const MetricNameSelector = memo(function MetricNameSelector({
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(currentMetricName || defaultValue || '');
|
||||
if (currentMetricName) {
|
||||
setSearchText(currentMetricName);
|
||||
}
|
||||
}, [defaultValue, currentMetricName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevSignalSourceRef.current !== signalSource) {
|
||||
const previousSignalSource = prevSignalSourceRef.current;
|
||||
prevSignalSourceRef.current = signalSource;
|
||||
|
||||
const isNormalizationTransition =
|
||||
(previousSignalSource === undefined && signalSource === '') ||
|
||||
(previousSignalSource === '' && signalSource === undefined);
|
||||
|
||||
if (isNormalizationTransition && currentMetricName) {
|
||||
setSearchText(currentMetricName);
|
||||
setInputValue(currentMetricName || defaultValue || '');
|
||||
return;
|
||||
}
|
||||
|
||||
setSearchText('');
|
||||
setInputValue('');
|
||||
}
|
||||
}, [signalSource]);
|
||||
}, [signalSource, currentMetricName, defaultValue]);
|
||||
|
||||
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
|
||||
|
||||
@@ -152,7 +167,9 @@ export const MetricNameSelector = memo(function MetricNameSelector({
|
||||
}, [metrics]);
|
||||
|
||||
useEffect(() => {
|
||||
const metricName = (query.aggregations?.[0] as MetricAggregation)?.metricName;
|
||||
const metricName =
|
||||
(query.aggregations?.[0] as MetricAggregation)?.metricName ||
|
||||
query.aggregateAttribute?.key;
|
||||
const hasAggregateAttributeType = query.aggregateAttribute?.type;
|
||||
|
||||
if (metricName && !hasAggregateAttributeType && metrics.length > 0) {
|
||||
@@ -164,7 +181,13 @@ export const MetricNameSelector = memo(function MetricNameSelector({
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [metrics, query.aggregations, query.aggregateAttribute?.type, onChange]);
|
||||
}, [
|
||||
metrics,
|
||||
query.aggregations,
|
||||
query.aggregateAttribute?.key,
|
||||
query.aggregateAttribute?.type,
|
||||
onChange,
|
||||
]);
|
||||
|
||||
const resolveMetricFromText = useCallback(
|
||||
(text: string): BaseAutocompleteData => {
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import {
|
||||
getMetricsTreeMap,
|
||||
MetricsTreeMapPayload,
|
||||
MetricsTreeMapResponse,
|
||||
} from 'api/metricsExplorer/getMetricsTreeMap';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
type UseGetMetricsTreeMap = (
|
||||
requestData: MetricsTreeMapPayload,
|
||||
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
|
||||
Error
|
||||
>,
|
||||
|
||||
headers?: Record<string, string>,
|
||||
) => UseQueryResult<
|
||||
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
|
||||
Error
|
||||
>;
|
||||
|
||||
export const useGetMetricsTreeMap: UseGetMetricsTreeMap = (
|
||||
requestData,
|
||||
options,
|
||||
headers,
|
||||
) => {
|
||||
const queryKey = useMemo(() => {
|
||||
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||
return [...options.queryKey];
|
||||
}
|
||||
|
||||
if (options?.queryKey && typeof options.queryKey === 'string') {
|
||||
return options.queryKey;
|
||||
}
|
||||
|
||||
return [REACT_QUERY_KEY.GET_METRICS_TREE_MAP, requestData];
|
||||
}, [options?.queryKey, requestData]);
|
||||
|
||||
return useQuery<
|
||||
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
|
||||
Error
|
||||
>({
|
||||
queryFn: ({ signal }) => getMetricsTreeMap(requestData, signal, headers),
|
||||
...options,
|
||||
queryKey,
|
||||
});
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import updateMetricMetadata, {
|
||||
UpdateMetricMetadataProps,
|
||||
UpdateMetricMetadataResponse,
|
||||
} from 'api/metricsExplorer/updateMetricMetadata';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
export interface UseUpdateMetricMetadataProps {
|
||||
metricName: string;
|
||||
payload: UpdateMetricMetadataProps;
|
||||
}
|
||||
|
||||
export function useUpdateMetricMetadata(): UseMutationResult<
|
||||
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
|
||||
Error,
|
||||
UseUpdateMetricMetadataProps
|
||||
> {
|
||||
return useMutation<
|
||||
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
|
||||
Error,
|
||||
UseUpdateMetricMetadataProps
|
||||
>({
|
||||
mutationFn: ({ metricName, payload }) =>
|
||||
updateMetricMetadata(metricName, payload),
|
||||
});
|
||||
}
|
||||
@@ -252,23 +252,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-details-v2 {
|
||||
.tabs-and-filters {
|
||||
.ant-tabs {
|
||||
.ant-tabs-tab {
|
||||
.ant-tabs-tab-btn {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-400) !important;
|
||||
|
||||
&[aria-selected='true'] {
|
||||
background: var(--bg-vanilla-200);
|
||||
color: var(--text-robin-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,25 +24,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.service-route-tab {
|
||||
.ant-tabs-nav {
|
||||
&::before {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
.ant-tabs-nav-list {
|
||||
.ant-tabs-tab {
|
||||
color: var(--text-ink-400);
|
||||
}
|
||||
|
||||
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: var(--text-robin-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user