Compare commits

...

14 Commits

Author SHA1 Message Date
amlannandy
1245ccdad3 chore: minor fix 2025-12-27 21:44:46 +07:00
amlannandy
4296ce156a chore: address cursor comments and other minor fixes 2025-12-27 21:44:46 +07:00
amlannandy
63ffbbc13b chore: replace component in dashboards 2025-12-27 21:44:46 +07:00
amlannandy
586430f477 chore: update component in old alerts page 2025-12-27 21:44:46 +07:00
amlannandy
9b6fc49b1c chore: fix rebase issue 2025-12-27 21:44:46 +07:00
amlannandy
cd404f3433 chore: fix issue 2025-12-27 21:44:46 +07:00
amlannandy
5bed2ae116 chore: fix issues 2025-12-27 21:44:33 +07:00
amlannandy
f0ca66387d chore: address comment 2025-12-27 21:43:10 +07:00
amlannandy
abb5c42c55 chore: address comment 2025-12-27 21:43:10 +07:00
amlannandy
3054b0d946 chore: warn users about incorrect usage with y axis unit 2025-12-27 21:43:10 +07:00
lif
4ae268d867 fix: improve light mode text color for selected values in query builder (#9876)
In light mode, selected values in query builder Select components appeared
disabled due to inheriting light-colored text from dark mode styles.

This fix adds explicit text color (--text-ink-400) for .ant-select-selection-item
elements in light mode across QueryBuilder, QueryBuilderV2, and
MetricsAggregateSection styles.

Fixes #9801

Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-12-26 17:38:25 +00:00
Amlan Kumar Nandy
9d78d67461 chore: y axis management in metrics explorer (#9587) 2025-12-26 17:14:15 +00:00
Abhi kumar
055d0ba90d fix: added fix for limit still getting sent in payload even after removing (#9877)
* fix: added fix for limit still getting sent in payload even after removing

* chore: removed console log
2025-12-26 17:35:08 +05:30
Abhi kumar
09dc95cfe9 fix: added fix for metric selection tooltip scroll issue (#9869) 2025-12-26 13:40:19 +05:30
35 changed files with 1681 additions and 133 deletions

View File

@@ -0,0 +1,29 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
export const getMetricMetadata = async (
metricName: string,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponseV2<MetricMetadataResponse> | ErrorResponseV2> => {
try {
const encodedMetricName = encodeURIComponent(metricName);
const response = await axios.get(
`/metrics/metadata?metricName=${encodedMetricName}`,
{
signal,
headers,
},
);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
return ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};

View File

@@ -560,6 +560,10 @@
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
.ant-select-selection-item {
color: var(--text-ink-400);
}
}
}
}
@@ -569,6 +573,10 @@
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1) !important;
.ant-select-selection-item {
color: var(--text-ink-400);
}
}
.ant-select-arrow {

View File

@@ -169,6 +169,10 @@
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
.ant-select-selection-item {
color: var(--text-ink-400);
}
}
}
}

View File

@@ -1,11 +1,18 @@
import './styles.scss';
import { Select } from 'antd';
import { WarningFilled } from '@ant-design/icons';
import { Select, Tooltip } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import classNames from 'classnames';
import { useMemo } from 'react';
import { UniversalYAxisUnitMappings } from './constants';
import { UniversalYAxisUnit, YAxisUnitSelectorProps } from './types';
import { getYAxisCategories, mapMetricUnitToUniversalUnit } from './utils';
import {
getUniversalNameFromMetricUnit,
getYAxisCategories,
mapMetricUnitToUniversalUnit,
} from './utils';
function YAxisUnitSelector({
value,
@@ -14,9 +21,24 @@ function YAxisUnitSelector({
loading = false,
'data-testid': dataTestId,
source,
initialValue,
}: YAxisUnitSelectorProps): JSX.Element {
const universalUnit = mapMetricUnitToUniversalUnit(value);
const incompatibleUnitMessage = useMemo(() => {
if (!initialValue || !value || loading) return '';
const initialUniversalUnit = mapMetricUnitToUniversalUnit(initialValue);
const currentUniversalUnit = mapMetricUnitToUniversalUnit(value);
if (initialUniversalUnit !== currentUniversalUnit) {
const initialUniversalUnitName = getUniversalNameFromMetricUnit(
initialValue,
);
const currentUniversalUnitName = getUniversalNameFromMetricUnit(value);
return `Unit mismatch. Saved unit is ${initialUniversalUnitName}, but ${currentUniversalUnitName} is selected.`;
}
return '';
}, [initialValue, value, loading]);
const handleSearch = (
searchTerm: string,
currentOption: DefaultOptionType | undefined,
@@ -49,6 +71,16 @@ function YAxisUnitSelector({
placeholder={placeholder}
filterOption={(input, option): boolean => handleSearch(input, option)}
loading={loading}
suffixIcon={
incompatibleUnitMessage ? (
<Tooltip title={incompatibleUnitMessage}>
<WarningFilled />
</Tooltip>
) : null
}
className={classNames({
'warning-state': incompatibleUnitMessage,
})}
data-testid={dataTestId}
>
{categories.map((category) => (

View File

@@ -91,4 +91,36 @@ describe('YAxisUnitSelector', () => {
expect(screen.getByText('Bytes (B)')).toBeInTheDocument();
expect(screen.getByText('Seconds (s)')).toBeInTheDocument();
});
it('shows warning message when incompatible unit is selected', () => {
render(
<YAxisUnitSelector
source={YAxisSource.ALERTS}
value="By"
onChange={mockOnChange}
initialValue="s"
/>,
);
const warningIcon = screen.getByLabelText('warning');
expect(warningIcon).toBeInTheDocument();
fireEvent.mouseOver(warningIcon);
return screen
.findByText(
'Unit mismatch. Saved unit is Seconds (s), but Bytes (B) is selected.',
)
.then((el) => expect(el).toBeInTheDocument());
});
it('does not show warning message when compatible unit is selected', () => {
render(
<YAxisUnitSelector
source={YAxisSource.ALERTS}
value="s"
onChange={mockOnChange}
initialValue="s"
/>,
);
const warningIcon = screen.queryByLabelText('warning');
expect(warningIcon).not.toBeInTheDocument();
});
});

View File

@@ -3,3 +3,13 @@
width: 220px;
}
}
.warning-state {
.ant-select-selector {
border-color: var(--bg-amber-400) !important;
}
.anticon {
color: var(--bg-amber-400) !important;
}
}

View File

@@ -6,6 +6,7 @@ export interface YAxisUnitSelectorProps {
disabled?: boolean;
'data-testid'?: string;
source: YAxisSource;
initialValue?: string;
}
export enum UniversalYAxisUnit {

View File

@@ -55,6 +55,7 @@ export const REACT_QUERY_KEY = {
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',
GET_RELATED_METRICS: 'GET_RELATED_METRICS',
GET_INSPECT_METRICS_DETAILS: 'GET_INSPECT_METRICS_DETAILS',
GET_METRIC_METADATA: 'GET_METRIC_METADATA',
// Traces Funnels Query Keys
GET_DOMAINS_LIST: 'GET_DOMAINS_LIST',

View File

@@ -5,7 +5,8 @@ import { useCreateAlertState } from 'container/CreateAlertV2/context';
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useState } from 'react';
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertDef } from 'types/api/alerts/def';
@@ -27,6 +28,17 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
const yAxisUnit = alertState.yAxisUnit || '';
const selectedQueryName = thresholdState.selectedQuery;
const { yAxisUnit: initialYAxisUnit, isLoading } = useGetYAxisUnit(
selectedQueryName,
);
useEffect(() => {
if (initialYAxisUnit && !yAxisUnit) {
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: initialYAxisUnit });
}
}, [initialYAxisUnit, setAlertState, yAxisUnit]);
const headline = (
<div className="chart-preview-headline">
<PlotTag
@@ -34,11 +46,13 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
<YAxisUnitSelector
value={alertState.yAxisUnit}
value={yAxisUnit}
initialValue={initialYAxisUnit}
onChange={(value): void => {
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
}}
source={YAxisSource.ALERTS}
loading={isLoading}
/>
</div>
);

View File

@@ -6,6 +6,8 @@ import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent';
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
@@ -14,9 +16,9 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
@@ -786,6 +788,16 @@ function FormAlertRules({
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
const { yAxisUnit: initialYAxisUnit, isLoading } = useGetYAxisUnit(
alertDef.condition.selectedQueryName,
);
useEffect(() => {
if (initialYAxisUnit && !yAxisUnit) {
setYAxisUnit(initialYAxisUnit);
}
}, [initialYAxisUnit, yAxisUnit]);
return (
<>
{Element}
@@ -841,9 +853,12 @@ function FormAlertRules({
</div>
<StepContainer>
<BuilderUnitsFilter
<YAxisUnitSelector
value={yAxisUnit}
initialValue={initialYAxisUnit}
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
source={YAxisSource.ALERTS}
loading={isLoading}
/>
</StepContainer>

View File

@@ -120,7 +120,6 @@ function FullView({
originalGraphType: selectedPanelType,
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,

View File

@@ -137,7 +137,6 @@ function GridCardGraph({
originalGraphType: widget.panelTypes,
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
const initialDataSource = updatedQuery.builder.queryData[0].dataSource;
return {
query: updatedQuery,

View File

@@ -58,6 +58,27 @@
.explore-content {
padding: 0 8px;
.y-axis-unit-selector-container {
display: flex;
align-items: center;
gap: 10px;
padding-top: 10px;
margin-bottom: 10px;
.save-unit-container {
display: flex;
align-items: center;
gap: 10px;
.ant-btn {
border-radius: 2px;
.ant-typography {
font-size: 12px;
}
}
}
}
.ant-space {
margin-top: 10px;
margin-bottom: 20px;
@@ -75,6 +96,14 @@
.time-series-view {
min-width: 100%;
width: 100%;
position: relative;
.no-unit-warning {
position: absolute;
top: 30px;
right: 40px;
z-index: 1000;
}
}
.time-series-container {

View File

@@ -1,7 +1,7 @@
import './Explorer.styles.scss';
import * as Sentry from '@sentry/react';
import { Switch } from 'antd';
import { Switch, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import WarningPopover from 'components/WarningPopover/WarningPopover';
@@ -25,10 +25,14 @@ import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToD
import { v4 as uuid } from 'uuid';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
// import QuerySection from './QuerySection';
import MetricDetails from '../MetricDetails/MetricDetails';
import TimeSeries from './TimeSeries';
import { ExplorerTabs } from './types';
import { splitQueryIntoOneChartPerQuery } from './utils';
import {
getMetricUnits,
splitQueryIntoOneChartPerQuery,
useGetMetrics,
} from './utils';
const ONE_CHART_PER_QUERY_ENABLED_KEY = 'isOneChartPerQueryEnabled';
@@ -40,6 +44,34 @@ function Explorer(): JSX.Element {
currentQuery,
} = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
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);
}
});
return currentMetricNames;
}, [stagedQuery]);
const {
metrics,
isLoading: isMetricUnitsLoading,
isError: isMetricUnitsError,
} = useGetMetrics(metricNames);
const units = useMemo(() => getMetricUnits(metrics), [metrics]);
const areAllMetricUnitsSame = useMemo(
() =>
!isMetricUnitsLoading &&
!isMetricUnitsError &&
units.length > 0 &&
units.every((unit) => unit && unit === units[0]),
[units, isMetricUnitsLoading, isMetricUnitsError],
);
const [searchParams, setSearchParams] = useSearchParams();
const isOneChartPerQueryEnabled =
@@ -48,7 +80,66 @@ function Explorer(): JSX.Element {
const [showOneChartPerQuery, toggleShowOneChartPerQuery] = useState(
isOneChartPerQueryEnabled,
);
const [disableOneChartPerQuery, toggleDisableOneChartPerQuery] = useState(
false,
);
const [selectedTab] = useState<ExplorerTabs>(ExplorerTabs.TIME_SERIES);
const [yAxisUnit, setYAxisUnit] = useState<string | undefined>();
const unitsLength = useMemo(() => units.length, [units]);
const firstUnit = useMemo(() => units?.[0], [units]);
useEffect(() => {
// Set the y axis unit to the first metric unit if
// 1. There is one metric unit and it is not empty
// 2. All metric units are the same and not empty
// Else, set the y axis unit to empty if
// 1. There are more than one metric units and they are not the same
// 2. There are no metric units
// 3. There is exactly one metric unit but it is empty/undefined
if (unitsLength === 0) {
setYAxisUnit(undefined);
} else if (unitsLength === 1 && firstUnit) {
setYAxisUnit(firstUnit);
} else if (unitsLength === 1 && !firstUnit) {
setYAxisUnit(undefined);
} else if (areAllMetricUnitsSame) {
if (firstUnit) {
setYAxisUnit(firstUnit);
} else {
setYAxisUnit(undefined);
}
} else if (unitsLength > 1 && !areAllMetricUnitsSame) {
setYAxisUnit(undefined);
}
}, [unitsLength, firstUnit, areAllMetricUnitsSame]);
useEffect(() => {
// Don't apply logic during loading to avoid overwriting user preferences
if (isMetricUnitsLoading) {
return;
}
// Disable one chart per query if -
// 1. There are more than one metric
// 2. The metric units are not the same
if (units.length > 1 && !areAllMetricUnitsSame) {
toggleShowOneChartPerQuery(true);
toggleDisableOneChartPerQuery(true);
} else if (units.length <= 1) {
toggleShowOneChartPerQuery(false);
toggleDisableOneChartPerQuery(true);
} else {
// When units are the same and loading is complete, restore URL-based preference
toggleShowOneChartPerQuery(isOneChartPerQueryEnabled);
toggleDisableOneChartPerQuery(false);
}
}, [
units,
areAllMetricUnitsSame,
isMetricUnitsLoading,
isOneChartPerQueryEnabled,
]);
const handleToggleShowOneChartPerQuery = (): void => {
toggleShowOneChartPerQuery(!showOneChartPerQuery);
@@ -68,15 +159,20 @@ function Explorer(): JSX.Element {
[updateAllQueriesOperators],
);
const exportDefaultQuery = useMemo(
() =>
updateAllQueriesOperators(
currentQuery || initialQueriesMap[DataSource.METRICS],
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
),
[currentQuery, updateAllQueriesOperators],
);
const exportDefaultQuery = useMemo(() => {
const query = updateAllQueriesOperators(
currentQuery || initialQueriesMap[DataSource.METRICS],
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
);
if (yAxisUnit && !query.unit) {
return {
...query,
unit: yAxisUnit,
};
}
return query;
}, [currentQuery, updateAllQueriesOperators, yAxisUnit]);
useShareBuilderUrl({ defaultValue: defaultQuery });
@@ -90,8 +186,16 @@ function Explorer(): JSX.Element {
const widgetId = uuid();
let query = queryToExport || exportDefaultQuery;
if (yAxisUnit && !query.unit) {
query = {
...query,
unit: yAxisUnit,
};
}
const dashboardEditView = generateExportToDashboardLink({
query: queryToExport || exportDefaultQuery,
query,
panelType: PANEL_TYPES.TIME_SERIES,
dashboardId: dashboard.id,
widgetId,
@@ -99,17 +203,33 @@ function Explorer(): JSX.Element {
safeNavigate(dashboardEditView);
},
[exportDefaultQuery, safeNavigate],
[exportDefaultQuery, safeNavigate, yAxisUnit],
);
const splitedQueries = useMemo(
() =>
splitQueryIntoOneChartPerQuery(
stagedQuery || initialQueriesMap[DataSource.METRICS],
metricNames,
units,
),
[stagedQuery],
[stagedQuery, metricNames, units],
);
const [selectedMetricName, setSelectedMetricName] = useState<string | null>(
null,
);
const handleOpenMetricDetails = (metricName: string): void => {
setIsMetricDetailsOpen(true);
setSelectedMetricName(metricName);
};
const handleCloseMetricDetails = (): void => {
setIsMetricDetailsOpen(false);
setSelectedMetricName(null);
};
useEffect(() => {
logEvent(MetricsExplorerEvents.TabChanged, {
[MetricsExplorerEventKeys.Tab]: 'explorer',
@@ -123,17 +243,44 @@ function Explorer(): JSX.Element {
const [warning, setWarning] = useState<Warning | undefined>(undefined);
const oneChartPerQueryDisabledTooltip = useMemo(() => {
if (splitedQueries.length <= 1) {
return 'One chart per query cannot be toggled for a single query.';
}
if (units.length <= 1) {
return 'One chart per query cannot be toggled when there is only one metric.';
}
if (disableOneChartPerQuery) {
return 'One chart per query cannot be disabled for multiple queries with different units.';
}
return undefined;
}, [disableOneChartPerQuery, splitedQueries.length, units.length]);
// Show the y axis unit selector if -
// 1. There is only one metric
// 2. The metric has no saved unit
const showYAxisUnitSelector = useMemo(
() => !isMetricUnitsLoading && units.length === 1 && !units[0],
[units, isMetricUnitsLoading],
);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-explore-container">
<div className="explore-header">
<div className="explore-header-left-actions">
<span>1 chart/query</span>
<Switch
checked={showOneChartPerQuery}
onChange={handleToggleShowOneChartPerQuery}
size="small"
/>
<Tooltip
open={disableOneChartPerQuery ? undefined : false}
title={oneChartPerQueryDisabledTooltip}
>
<Switch
checked={showOneChartPerQuery}
onChange={handleToggleShowOneChartPerQuery}
disabled={disableOneChartPerQuery || splitedQueries.length <= 1}
size="small"
/>
</Tooltip>
</div>
<div className="explore-header-right-actions">
{!isEmpty(warning) && <WarningPopover warningData={warning} />}
@@ -174,6 +321,16 @@ function Explorer(): JSX.Element {
<TimeSeries
showOneChartPerQuery={showOneChartPerQuery}
setWarning={setWarning}
areAllMetricUnitsSame={areAllMetricUnitsSame}
isMetricUnitsLoading={isMetricUnitsLoading}
isMetricUnitsError={isMetricUnitsError}
metricUnits={units}
metricNames={metricNames}
metrics={metrics}
handleOpenMetricDetails={handleOpenMetricDetails}
yAxisUnit={yAxisUnit}
setYAxisUnit={setYAxisUnit}
showYAxisUnitSelector={showYAxisUnitSelector}
/>
)}
{/* TODO: Enable once we have resolved all related metrics issues */}
@@ -187,9 +344,17 @@ function Explorer(): JSX.Element {
query={exportDefaultQuery}
sourcepage={DataSource.METRICS}
onExport={handleExport}
isOneChartPerQuery={false}
isOneChartPerQuery={showOneChartPerQuery}
splitedQueries={splitedQueries}
/>
{isMetricDetailsOpen && (
<MetricDetails
metricName={selectedMetricName}
isOpen={isMetricDetailsOpen}
onClose={handleCloseMetricDetails}
isModalTimeSelection={false}
/>
)}
</Sentry.ErrorBoundary>
);
}

View File

@@ -1,14 +1,18 @@
import { Color } from '@signozhq/design-tokens';
import { Tooltip, Typography } from 'antd';
import { isAxiosError } from 'axios';
import classNames from 'classnames';
import YAxisUnitSelector from 'components/YAxisUnitSelector';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter/BuilderUnits';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useMemo, useState } from 'react';
import { AlertTriangle } from 'lucide-react';
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -24,6 +28,13 @@ import { splitQueryIntoOneChartPerQuery } from './utils';
function TimeSeries({
showOneChartPerQuery,
setWarning,
isMetricUnitsLoading,
metricUnits,
metricNames,
handleOpenMetricDetails,
yAxisUnit,
setYAxisUnit,
showYAxisUnitSelector,
}: TimeSeriesProps): JSX.Element {
const { stagedQuery, currentQuery } = useQueryBuilder();
@@ -56,13 +67,14 @@ function TimeSeries({
showOneChartPerQuery
? splitQueryIntoOneChartPerQuery(
stagedQuery || initialQueriesMap[DataSource.METRICS],
metricNames,
metricUnits,
)
: [stagedQuery || initialQueriesMap[DataSource.METRICS]],
[showOneChartPerQuery, stagedQuery],
// eslint-disable-next-line react-hooks/exhaustive-deps
[showOneChartPerQuery, stagedQuery, JSON.stringify(metricUnits)],
);
const [yAxisUnit, setYAxisUnit] = useState<string>('');
const queries = useQueries(
queryPayloads.map((payload, index) => ({
queryKey: [
@@ -126,32 +138,148 @@ function TimeSeries({
setYAxisUnit(value);
};
// TODO: Enable once we have resolved all related metrics v2 api issues
// Show the save unit button if
// 1. There is only one metric
// 2. The metric has no saved unit
// 3. The user has selected a unit
// const showSaveUnitButton = useMemo(
// () =>
// metricUnits.length === 1 &&
// Boolean(metrics?.[0]) &&
// !metricUnits[0] &&
// yAxisUnit,
// [metricUnits, metrics, yAxisUnit],
// );
// const {
// mutate: updateMetricMetadata,
// isLoading: isUpdatingMetricMetadata,
// } = useUpdateMetricMetadata();
// const handleSaveUnit = (): void => {
// updateMetricMetadata(
// {
// metricName: metricNames[0],
// payload: {
// unit: yAxisUnit,
// description: metrics[0]?.description ?? '',
// metricType: metrics[0]?.type as MetricType,
// temporality: metrics[0]?.temporality,
// },
// },
// {
// onSuccess: () => {
// notifications.success({
// message: 'Unit saved successfully',
// });
// queryClient.invalidateQueries([
// REACT_QUERY_KEY.GET_METRIC_DETAILS,
// metricNames[0],
// ]);
// },
// onError: () => {
// notifications.error({
// message: 'Failed to save unit',
// });
// },
// },
// );
// };
return (
<>
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
<div className="y-axis-unit-selector-container">
{showYAxisUnitSelector && (
<>
<YAxisUnitSelector
onChange={onUnitChangeHandler}
value={yAxisUnit}
source={YAxisSource.EXPLORER}
data-testid="y-axis-unit-selector"
/>
{/* TODO: Enable once we have resolved all related metrics v2 api issues */}
{/* {showSaveUnitButton && (
<div className="save-unit-container">
<Typography.Text>
Save the selected unit for this metric?
</Typography.Text>
<Button
type="primary"
size="small"
disabled={isUpdatingMetricMetadata}
onClick={handleSaveUnit}
>
<Typography.Paragraph>Yes</Typography.Paragraph>
</Button>
</div>
)} */}
</>
)}
</div>
<div
className={classNames({
'time-series-container': changeLayoutForOneChartPerQuery,
})}
>
{responseData.map((datapoint, index) => (
<div
className="time-series-view"
// eslint-disable-next-line react/no-array-index-key
key={index}
>
<TimeSeriesView
isFilterApplied={false}
isError={queries[index].isError}
isLoading={queries[index].isLoading}
data={datapoint}
yAxisUnit={yAxisUnit}
dataSource={DataSource.METRICS}
error={queries[index].error as APIError}
setWarning={setWarning}
/>
</div>
))}
{responseData.map((datapoint, index) => {
const isQueryDataItem = index < metricNames.length;
const metricName = isQueryDataItem ? metricNames[index] : undefined;
const metricUnit = isQueryDataItem ? metricUnits[index] : undefined;
// Show the no unit warning if -
// 1. The metric query is not loading
// 2. The metric units are not loading
// 3. There are more than one metric
// 4. The current metric unit is empty
// 5. Is a queryData item
const isMetricUnitEmpty =
isQueryDataItem &&
!queries[index].isLoading &&
!isMetricUnitsLoading &&
metricUnits.length > 1 &&
!metricUnit &&
metricName;
const currentYAxisUnit = yAxisUnit || metricUnit;
return (
<div
className="time-series-view"
// eslint-disable-next-line react/no-array-index-key
key={index}
>
{isMetricUnitEmpty && metricName && (
<Tooltip
className="no-unit-warning"
title={
<Typography.Text>
This metric does not have a unit. Please set one for it in the{' '}
<Typography.Link
onClick={(): void => handleOpenMetricDetails(metricName)}
>
metric details
</Typography.Link>{' '}
page.
</Typography.Text>
}
>
<AlertTriangle size={16} color={Color.BG_AMBER_400} />
</Tooltip>
)}
<TimeSeriesView
isFilterApplied={false}
isError={queries[index].isError}
isLoading={queries[index].isLoading || isMetricUnitsLoading}
data={datapoint}
yAxisUnit={currentYAxisUnit}
dataSource={DataSource.METRICS}
error={queries[index].error as APIError}
setWarning={setWarning}
/>
</div>
);
})}
</div>
</>
);

View File

@@ -1,4 +1,6 @@
import { render, screen } from '@testing-library/react';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import * as useOptionsMenuHooks from 'container/OptionsMenu';
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
@@ -12,13 +14,18 @@ import { MemoryRouter } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom-v5-compat';
import store from 'store';
import { LicenseEvent } from 'types/api/licensesV3/getActive';
import { DataSource } from 'types/common/queryBuilder';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import Explorer from '../Explorer';
import * as useGetMetricsHooks from '../utils';
const mockSetSearchParams = jest.fn();
const queryClient = new QueryClient();
const mockUpdateAllQueriesOperators = jest.fn();
const mockUpdateAllQueriesOperators = jest
.fn()
.mockReturnValue(initialQueriesMap[DataSource.METRICS]);
const mockUseQueryBuilderData = {
handleRunQuery: jest.fn(),
stagedQuery: initialQueriesMap[DataSource.METRICS],
@@ -126,6 +133,30 @@ jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
...mockUseQueryBuilderData,
} as any);
const Y_AXIS_UNIT_SELECTOR_TEST_ID = 'y-axis-unit-selector';
const mockMetric: MetricMetadata = {
type: MetricType.SUM,
description: 'metric1 description',
unit: 'metric1 unit',
temporality: Temporality.CUMULATIVE,
isMonotonic: true,
};
function renderExplorer(): void {
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<ErrorModalProvider>
<Explorer />
</ErrorModalProvider>
</Provider>
</MemoryRouter>
</QueryClientProvider>,
);
}
describe('Explorer', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -142,17 +173,7 @@ describe('Explorer', () => {
mockSetSearchParams,
]);
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<ErrorModalProvider>
<Explorer />
</ErrorModalProvider>
</Provider>
</MemoryRouter>
</QueryClientProvider>,
);
renderExplorer();
expect(mockUpdateAllQueriesOperators).toHaveBeenCalledWith(
initialQueriesMap[DataSource.METRICS],
@@ -166,18 +187,13 @@ describe('Explorer', () => {
new URLSearchParams({ isOneChartPerQueryEnabled: 'true' }),
mockSetSearchParams,
]);
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric, mockMetric],
});
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<ErrorModalProvider>
<Explorer />
</ErrorModalProvider>
</Provider>
</MemoryRouter>
</QueryClientProvider>,
);
renderExplorer();
const toggle = screen.getByRole('switch');
expect(toggle).toBeChecked();
@@ -188,20 +204,132 @@ describe('Explorer', () => {
new URLSearchParams({ isOneChartPerQueryEnabled: 'false' }),
mockSetSearchParams,
]);
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric, mockMetric],
});
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Provider store={store}>
<ErrorModalProvider>
<Explorer />
</ErrorModalProvider>
</Provider>
</MemoryRouter>
</QueryClientProvider>,
);
renderExplorer();
const toggle = screen.getByRole('switch');
expect(toggle).not.toBeChecked();
});
it('should not render y axis unit selector for single metric which has a unit', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric],
});
renderExplorer();
const yAxisUnitSelector = screen.queryByTestId(Y_AXIS_UNIT_SELECTOR_TEST_ID);
expect(yAxisUnitSelector).not.toBeInTheDocument();
});
it('should not render y axis unit selector for mutliple metrics with same unit', () => {
(useSearchParams as jest.Mock).mockReturnValueOnce([
new URLSearchParams({ isOneChartPerQueryEnabled: 'true' }),
mockSetSearchParams,
]);
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric, mockMetric],
});
renderExplorer();
const yAxisUnitSelector = screen.queryByTestId(Y_AXIS_UNIT_SELECTOR_TEST_ID);
expect(yAxisUnitSelector).not.toBeInTheDocument();
});
it('should hide y axis unit selector for multiple metrics with different units', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric, mockMetric],
});
renderExplorer();
const yAxisUnitSelector = screen.queryByTestId(Y_AXIS_UNIT_SELECTOR_TEST_ID);
expect(yAxisUnitSelector).not.toBeInTheDocument();
// One chart per query toggle should be disabled
const oneChartPerQueryToggle = screen.getByRole('switch');
expect(oneChartPerQueryToggle).toBeDisabled();
});
it('should render empty y axis unit selector for a single metric with no unit', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [
{
type: MetricType.SUM,
description: 'metric1 description',
unit: '',
temporality: Temporality.CUMULATIVE,
isMonotonic: true,
},
],
});
renderExplorer();
const yAxisUnitSelector = screen.queryByTestId(Y_AXIS_UNIT_SELECTOR_TEST_ID);
expect(yAxisUnitSelector).toBeInTheDocument();
expect(yAxisUnitSelector).toHaveTextContent('Please select a unit');
});
it('one chart per query should be off and disabled when there is only one query', () => {
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric],
});
renderExplorer();
const oneChartPerQueryToggle = screen.getByRole('switch');
expect(oneChartPerQueryToggle).not.toBeChecked();
expect(oneChartPerQueryToggle).toBeDisabled();
});
it('one chart per query should enabled by default when there are multiple metrics with the same unit', () => {
const mockQueryData = {
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
aggregateAttribute: {
...(initialQueriesMap[DataSource.METRICS].builder.queryData[0]
.aggregateAttribute as BaseAutocompleteData),
key: 'metric1',
},
};
const mockStagedQueryWithMultipleQueries = {
...initialQueriesMap[DataSource.METRICS],
builder: {
...initialQueriesMap[DataSource.METRICS].builder,
queryData: [mockQueryData, mockQueryData],
},
};
jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue(({
...mockUseQueryBuilderData,
stagedQuery: mockStagedQueryWithMultipleQueries,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
isLoading: false,
isError: false,
metrics: [mockMetric, mockMetric],
});
renderExplorer();
const oneChartPerQueryToggle = screen.getByRole('switch');
expect(oneChartPerQueryToggle).toBeEnabled();
});
});

View File

@@ -0,0 +1,180 @@
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { UpdateMetricMetadataResponse } from 'api/metricsExplorer/updateMetricMetadata';
import * as useUpdateMetricMetadataHooks from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import { UseUpdateMetricMetadataProps } from 'hooks/metricsExplorer/useUpdateMetricMetadata';
import { UseMutationResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import TimeSeries from '../TimeSeries';
import { TimeSeriesProps } from '../types';
type MockUpdateMetricMetadata = UseMutationResult<
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
Error,
UseUpdateMetricMetadataProps
>;
const mockUpdateMetricMetadata = jest.fn();
jest
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
.mockReturnValue(({
mutate: mockUpdateMetricMetadata,
isLoading: false,
} as Partial<MockUpdateMetricMetadata>) as MockUpdateMetricMetadata);
jest.mock('container/TimeSeriesView/TimeSeriesView', () => ({
__esModule: true,
default: jest.fn().mockReturnValue(
<div role="img" aria-label="warning">
TimeSeriesView
</div>,
),
}));
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQueryClient: jest.fn().mockReturnValue({
invalidateQueries: jest.fn(),
}),
useQueries: jest.fn().mockImplementation((queries: any[]) =>
queries.map(() => ({
data: undefined,
isLoading: false,
isError: false,
error: undefined,
})),
),
}));
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn().mockReturnValue({
globalTime: {
selectedTime: '5min',
maxTime: 1713738000000,
minTime: 1713734400000,
},
}),
}));
const mockMetric: MetricMetadata = {
type: MetricType.SUM,
description: 'metric1 description',
unit: 'metric1 unit',
temporality: Temporality.CUMULATIVE,
isMonotonic: true,
};
const mockSetWarning = jest.fn();
const mockSetIsMetricDetailsOpen = jest.fn();
const mockSetYAxisUnit = jest.fn();
function renderTimeSeries(
overrides: Partial<TimeSeriesProps> = {},
): RenderResult {
return render(
<TimeSeries
showOneChartPerQuery={false}
setWarning={mockSetWarning}
areAllMetricUnitsSame={false}
isMetricUnitsLoading={false}
metricUnits={[]}
metricNames={[]}
metrics={[]}
isMetricUnitsError={false}
handleOpenMetricDetails={mockSetIsMetricDetailsOpen}
yAxisUnit="count"
setYAxisUnit={mockSetYAxisUnit}
showYAxisUnitSelector={false}
// eslint-disable-next-line react/jsx-props-no-spreading
{...overrides}
/>,
);
}
describe('TimeSeries', () => {
it('should render a warning icon when a metric has no unit among multiple metrics', () => {
const user = userEvent.setup();
const { container } = renderTimeSeries({
metricUnits: ['', 'count'],
metricNames: ['metric1', 'metric2'],
metrics: [undefined, undefined],
});
const alertIcon = container.querySelector('.no-unit-warning') as HTMLElement;
user.hover(alertIcon);
waitFor(() =>
expect(
screen.findByText('This metric does not have a unit'),
).toBeInTheDocument(),
);
});
it('clicking on warning icon tooltip should open metric details modal', async () => {
const user = userEvent.setup();
const { container } = renderTimeSeries({
metricUnits: ['', 'count'],
metricNames: ['metric1', 'metric2'],
metrics: [mockMetric, mockMetric],
yAxisUnit: 'seconds',
});
const alertIcon = container.querySelector('.no-unit-warning') as HTMLElement;
user.hover(alertIcon);
const metricDetailsLink = await screen.findByText('metric details');
user.click(metricDetailsLink);
waitFor(() =>
expect(mockSetIsMetricDetailsOpen).toHaveBeenCalledWith('metric1'),
);
});
// TODO: Unskip this test once the save unit button is implemented
// Tracking at - https://github.com/SigNoz/engineering-pod/issues/3495
it.skip('shows Save unit button when metric had no unit but one is selected', () => {
const { findByText, getByRole } = renderTimeSeries({
metricUnits: [undefined],
metricNames: ['metric1'],
metrics: [mockMetric],
yAxisUnit: 'seconds',
});
expect(
findByText('Save the selected unit for this metric?'),
).toBeInTheDocument();
const yesButton = getByRole('button', { name: 'Yes' });
expect(yesButton).toBeInTheDocument();
expect(yesButton).toBeEnabled();
});
// TODO: Unskip this test once the save unit button is implemented
// Tracking at - https://github.com/SigNoz/engineering-pod/issues/3495
it.skip('clicking on save unit button shoould upated metric metadata', () => {
const user = userEvent.setup();
const { getByRole } = renderTimeSeries({
metricUnits: [''],
metricNames: ['metric1'],
metrics: [mockMetric],
yAxisUnit: 'seconds',
});
const yesButton = getByRole('button', { name: /Yes/i });
user.click(yesButton);
expect(mockUpdateMetricMetadata).toHaveBeenCalledWith(
{
metricName: 'metric1',
payload: expect.objectContaining({ unit: 'seconds' }),
},
expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
}),
);
});
});

View File

@@ -0,0 +1,161 @@
import { renderHook } from '@testing-library/react';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { initialQueriesMap } from 'constants/queryBuilder';
import * as useGetMultipleMetricsHook from 'hooks/metricsExplorer/useGetMultipleMetrics';
import { UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import {
MetricMetadata,
MetricMetadataResponse,
} from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderFormula,
IBuilderQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
getMetricUnits,
splitQueryIntoOneChartPerQuery,
useGetMetrics,
} from '../utils';
const MOCK_QUERY_DATA_1: IBuilderQuery = {
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
aggregateAttribute: {
...(initialQueriesMap[DataSource.METRICS].builder.queryData[0]
.aggregateAttribute as BaseAutocompleteData),
key: 'metric1',
},
};
const MOCK_QUERY_DATA_2: IBuilderQuery = {
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
aggregateAttribute: {
...(initialQueriesMap[DataSource.METRICS].builder.queryData[0]
.aggregateAttribute as BaseAutocompleteData),
key: 'metric2',
},
};
const MOCK_FORMULA_DATA: IBuilderFormula = {
expression: '1 + 1',
disabled: false,
queryName: 'Mock Formula',
legend: 'Mock Legend',
};
const MOCK_QUERY_WITH_MULTIPLE_QUERY_DATA: Query = {
...initialQueriesMap[DataSource.METRICS],
builder: {
...initialQueriesMap[DataSource.METRICS].builder,
queryData: [MOCK_QUERY_DATA_1, MOCK_QUERY_DATA_2],
queryFormulas: [MOCK_FORMULA_DATA, MOCK_FORMULA_DATA],
},
};
describe('splitQueryIntoOneChartPerQuery', () => {
it('should split a query with multiple queryData to multiple distinct queries, each with a single queryData', () => {
const result = splitQueryIntoOneChartPerQuery(
MOCK_QUERY_WITH_MULTIPLE_QUERY_DATA,
['metric1', 'metric2'],
[undefined, 'unit2'],
);
expect(result).toHaveLength(4);
// Verify query 1 has the correct data
expect(result[0].builder.queryData).toHaveLength(1);
expect(result[0].builder.queryData[0]).toEqual(MOCK_QUERY_DATA_1);
expect(result[0].builder.queryFormulas).toHaveLength(0);
expect(result[0].unit).toBeUndefined();
// Verify query 2 has the correct data
expect(result[1].builder.queryData).toHaveLength(1);
expect(result[1].builder.queryData[0]).toEqual(MOCK_QUERY_DATA_2);
expect(result[1].builder.queryFormulas).toHaveLength(0);
expect(result[1].unit).toBe('unit2');
// Verify query 3 has the correct data
expect(result[2].builder.queryFormulas).toHaveLength(1);
expect(result[2].builder.queryFormulas[0]).toEqual(MOCK_FORMULA_DATA);
expect(result[2].builder.queryData).toHaveLength(2); // 2 disabled queries
expect(result[2].builder.queryData[0].disabled).toBe(true);
expect(result[2].builder.queryData[1].disabled).toBe(true);
expect(result[2].unit).toBeUndefined();
// Verify query 4 has the correct data
expect(result[3].builder.queryFormulas).toHaveLength(1);
expect(result[3].builder.queryFormulas[0]).toEqual(MOCK_FORMULA_DATA);
expect(result[3].builder.queryData).toHaveLength(2); // 2 disabled queries
expect(result[3].builder.queryData[0].disabled).toBe(true);
expect(result[3].builder.queryData[1].disabled).toBe(true);
expect(result[3].unit).toBeUndefined();
});
});
const MOCK_METRIC_METADATA: MetricMetadata = {
description: 'Metric 1 description',
unit: 'unit1',
type: MetricType.GAUGE,
temporality: Temporality.DELTA,
isMonotonic: true,
};
describe('useGetMetrics', () => {
beforeEach(() => {
jest
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
.mockReturnValue([
({
isLoading: false,
isError: false,
data: {
httpStatusCode: 200,
data: {
status: 'success',
data: MOCK_METRIC_METADATA,
},
},
} as Partial<
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
]);
});
it('should return the correct metrics data', () => {
const { result } = renderHook(() => useGetMetrics(['metric1']));
expect(result.current.metrics).toHaveLength(1);
expect(result.current.metrics[0]).toBeDefined();
expect(result.current.metrics[0]).toEqual(MOCK_METRIC_METADATA);
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(false);
});
it('should return array of undefined values of correct length when metrics data is not yet loaded', () => {
jest
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
.mockReturnValue([
({
isLoading: true,
isError: false,
} as Partial<
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
]);
const { result } = renderHook(() => useGetMetrics(['metric1']));
expect(result.current.metrics).toHaveLength(1);
expect(result.current.metrics[0]).toBeUndefined();
});
});
describe('getMetricUnits', () => {
it('should return the same unit for units that are not known to the universal unit mapper', () => {
const result = getMetricUnits([MOCK_METRIC_METADATA]);
expect(result).toHaveLength(1);
expect(result[0]).toEqual(MOCK_METRIC_METADATA.unit);
});
it('should return universal unit for units that are known to the universal unit mapper', () => {
const result = getMetricUnits([{ ...MOCK_METRIC_METADATA, unit: 'seconds' }]);
expect(result).toHaveLength(1);
expect(result[0]).toBe('s');
});
});

View File

@@ -3,6 +3,7 @@ import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { SuccessResponse, Warning } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
export enum ExplorerTabs {
TIME_SERIES = 'time-series',
@@ -12,6 +13,16 @@ export enum ExplorerTabs {
export interface TimeSeriesProps {
showOneChartPerQuery: boolean;
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
areAllMetricUnitsSame: boolean;
isMetricUnitsLoading: boolean;
isMetricUnitsError: boolean;
metricUnits: (string | undefined)[];
metricNames: string[];
metrics: (MetricMetadata | undefined)[];
handleOpenMetricDetails: (metricName: string) => void;
yAxisUnit: string | undefined;
setYAxisUnit: (unit: string) => void;
showYAxisUnitSelector: boolean;
}
export interface RelatedMetricsProps {

View File

@@ -1,20 +1,40 @@
import { mapMetricUnitToUniversalUnit } from 'components/YAxisUnitSelector/utils';
import { useGetMultipleMetrics } from 'hooks/metricsExplorer/useGetMultipleMetrics';
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
export const splitQueryIntoOneChartPerQuery = (query: Query): Query[] => {
/**
* Split a query with multiple queryData to multiple distinct queries, each with a single queryData.
* @param query - The query to split
* @param units - The units of the metrics, can be undefined if the metric has no unit
* @returns The split queries
*/
export const splitQueryIntoOneChartPerQuery = (
query: Query,
metricNames: string[],
units: (string | undefined)[],
): Query[] => {
const queries: Query[] = [];
query.builder.queryData.forEach((currentQuery) => {
const newQuery = {
...query,
id: uuid(),
builder: {
...query.builder,
queryData: [currentQuery],
queryFormulas: [],
},
};
queries.push(newQuery);
if (currentQuery.aggregateAttribute?.key) {
const metricIndex = metricNames.indexOf(
currentQuery.aggregateAttribute?.key,
);
const unit = metricIndex >= 0 ? units[metricIndex] : undefined;
const newQuery = {
...query,
id: uuid(),
builder: {
...query.builder,
queryData: [currentQuery],
queryFormulas: [],
},
unit,
};
queries.push(newQuery);
}
});
query.builder.queryFormulas.forEach((currentFormula) => {
@@ -35,3 +55,43 @@ export const splitQueryIntoOneChartPerQuery = (query: Query): Query[] => {
return queries;
};
/**
* Hook to get data for multiple metrics with a synchronous loading and error state
* @param metricNames - The names of the metrics to get
* @param isEnabled - Whether the hook is enabled
* @returns The loading state, the metrics data, and the error state
*/
export function useGetMetrics(
metricNames: string[],
isEnabled = true,
): {
isLoading: boolean;
isError: boolean;
metrics: (MetricMetadata | undefined)[];
} {
const metricsData = useGetMultipleMetrics(metricNames, {
enabled: metricNames.length > 0 && isEnabled,
});
return {
isLoading: metricsData.some((metric) => metric.isLoading),
metrics: metricsData
.map((metric) => metric.data?.data)
.map((data) => data?.data),
isError: metricsData.some((metric) => metric.isError),
};
}
/**
* To get the units of the metrics in the universal unit standard.
* If the unit is not known to the universal unit mapper, it will return the unit as is.
* @param metrics - The metrics to get the units for
* @returns The units of the metrics, can be undefined if the metric has no unit
*/
export function getMetricUnits(
metrics: (MetricMetadata | undefined)[],
): (string | undefined)[] {
return metrics
.map((metric) => metric?.unit)
.map((unit) => mapMetricUnitToUniversalUnit(unit) || undefined);
}

View File

@@ -131,8 +131,8 @@ function MetricDetails({
>
Open in Explorer
</Button>
{/* Show the based on the feature flag. Will remove before releasing the feature */}
{showInspectFeature && (
{/* Show the inspect button if the metric type is GAUGE */}
{showInspectFeature && openInspectModal && (
<Button
className="inspect-metrics-button"
aria-label="Inspect Metric"

View File

@@ -11,7 +11,7 @@ export interface MetricDetailsProps {
isOpen: boolean;
metricName: string | null;
isModalTimeSelection: boolean;
openInspectModal: (metricName: string) => void;
openInspectModal?: (metricName: string) => void;
}
export interface DashboardsAndAlertsPopoverProps {

View File

@@ -191,7 +191,8 @@
text-transform: uppercase;
}
.y-axis-unit-selector {
.y-axis-unit-selector,
.y-axis-unit-selector-v2 {
margin-top: 16px;
display: flex;
flex-direction: column;
@@ -224,6 +225,21 @@
}
}
}
.y-axis-unit-selector-v2 {
.y-axis-unit-selector-component {
.ant-select {
width: 100%;
border-radius: 2px;
.ant-select-selector {
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
}
}
.soft-min-max {
display: flex;
align-items: center;

View File

@@ -0,0 +1,40 @@
import { Typography } from 'antd';
import YAxisUnitSelectorComponent from 'components/YAxisUnitSelector';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
import { Dispatch, SetStateAction, useEffect } from 'react';
type OnSelectType = Dispatch<SetStateAction<string>> | ((val: string) => void);
function YAxisUnitSelectorV2({
defaultValue,
onSelect,
fieldLabel,
}: {
defaultValue: string;
onSelect: OnSelectType;
fieldLabel: string;
}): JSX.Element {
const { yAxisUnit: initialYAxisUnit, isLoading } = useGetYAxisUnit();
useEffect(() => {
if (initialYAxisUnit && (!defaultValue || defaultValue === 'none')) {
onSelect(initialYAxisUnit);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialYAxisUnit]);
return (
<div className="y-axis-unit-selector-v2">
<Typography.Text className="heading">{fieldLabel}</Typography.Text>
<YAxisUnitSelectorComponent
value={defaultValue}
onChange={onSelect}
initialValue={initialYAxisUnit}
source={YAxisSource.DASHBOARDS}
loading={isLoading}
/>
</div>
);
}
export default YAxisUnitSelectorV2;

View File

@@ -65,7 +65,7 @@ import LegendColors from './LegendColors/LegendColors';
import ThresholdSelector from './Threshold/ThresholdSelector';
import { ThresholdProps } from './Threshold/types';
import { timePreferance } from './timeItems';
import YAxisUnitSelector from './YAxisUnitSelector';
import YAxisUnitSelectorV2 from './YAxisUnitSelectorV2';
const { TextArea } = Input;
const { Option } = Select;
@@ -350,9 +350,9 @@ function RightContainer({
)}
{allowYAxisUnit && (
<YAxisUnitSelector
<YAxisUnitSelectorV2
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
defaultValue={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE

View File

@@ -370,10 +370,6 @@ function NewWidget({
// this has been moved here from the left container
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
const updatedQuery = cloneDeep(stagedQuery || initialQueriesMap.metrics);
if (updatedQuery?.builder?.queryData?.[0]) {
updatedQuery.builder.queryData[0].pageSize = 10;
}
if (selectedWidget) {
if (selectedGraph === PANEL_TYPES.LIST) {
return {
@@ -419,16 +415,12 @@ function NewWidget({
useEffect(() => {
if (stagedQuery) {
setIsLoadingPanelData(false);
const updatedStagedQuery = cloneDeep(stagedQuery);
if (updatedStagedQuery?.builder?.queryData?.[0]) {
updatedStagedQuery.builder.queryData[0].pageSize = 10;
}
setRequestData((prev) => ({
...prev,
selectedTime: selectedTime.enum || prev.selectedTime,
globalSelectedInterval: customGlobalSelectedInterval,
graphType: getGraphType(selectedGraph || selectedWidget.panelTypes),
query: updatedStagedQuery,
query: stagedQuery,
fillGaps: selectedWidget.fillSpans || false,
isLogScale: selectedWidget.isLogScale || false,
formatForWeb:

View File

@@ -206,6 +206,10 @@
.ant-select-selector {
border-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
.ant-select-selection-item {
color: var(--text-ink-400);
}
}
.ant-input-number {

View File

@@ -0,0 +1,16 @@
.selectOptionContainer {
display: flex;
gap: 8px;
justify-content: space-between;
align-items: center;
overflow-x: auto;
&::-webkit-scrollbar {
width: 0.2rem;
height: 0.2rem;
}
}
.option-renderer-tooltip {
pointer-events: none;
}

View File

@@ -1,4 +1,4 @@
import './QueryBuilderSearch.styles.scss';
import './OptionRenderer.styles.scss';
import { Tooltip } from 'antd';
@@ -13,7 +13,11 @@ function OptionRenderer({
return (
<span className="option">
{type ? (
<Tooltip title={`${value}`} placement="topLeft">
<Tooltip
title={`${value}`}
placement="topLeft"
rootClassName="option-renderer-tooltip"
>
<div className="selectOptionContainer">
<div className="option-value">{value}</div>
<div className="option-meta-data-container">
@@ -29,7 +33,11 @@ function OptionRenderer({
</div>
</Tooltip>
) : (
<Tooltip title={label} placement="topLeft">
<Tooltip
title={label}
placement="topLeft"
rootClassName="option-renderer-tooltip"
>
<span>{label}</span>
</Tooltip>
)}

View File

@@ -5,19 +5,6 @@
gap: 12px;
}
.selectOptionContainer {
display: flex;
gap: 8px;
justify-content: space-between;
align-items: center;
overflow-x: auto;
&::-webkit-scrollbar {
width: 0.2rem;
height: 0.2rem;
}
}
.logs-popup {
&.hide-scroll {
.rc-virtual-list-holder {

View File

@@ -0,0 +1,32 @@
import { getMetricMetadata } from 'api/metricsExplorer/v2/getMetricMetadata';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQueries, UseQueryOptions, UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
type QueryResult = UseQueryResult<
SuccessResponseV2<MetricMetadataResponse>,
Error
>;
type UseGetMultipleMetrics = (
metricNames: string[],
options?: UseQueryOptions<SuccessResponseV2<MetricMetadataResponse>, Error>,
headers?: Record<string, string>,
) => QueryResult[];
export const useGetMultipleMetrics: UseGetMultipleMetrics = (
metricNames,
options,
headers,
) =>
useQueries(
metricNames.map(
(metricName) =>
({
queryKey: [REACT_QUERY_KEY.GET_METRIC_METADATA, metricName],
queryFn: ({ signal }) => getMetricMetadata(metricName, signal, headers),
...options,
} as UseQueryOptions<SuccessResponseV2<MetricMetadataResponse>, Error>),
),
);

View File

@@ -5,7 +5,7 @@ import updateMetricMetadata, {
import { useMutation, UseMutationResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
interface UseUpdateMetricMetadataProps {
export interface UseUpdateMetricMetadataProps {
metricName: string;
payload: UpdateMetricMetadataProps;
}

View File

@@ -0,0 +1,351 @@
import { renderHook } from '@testing-library/react';
import { useGetMetricUnits } from 'container/MetricsExplorer/Explorer/utils';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
import { useQueryBuilder } from './queryBuilder/useQueryBuilder';
import useGetYAxisUnit from './useGetYAxisUnit';
jest.mock('./queryBuilder/useQueryBuilder');
jest.mock('container/MetricsExplorer/Explorer/utils', () => ({
useGetMetricUnits: jest.fn(),
}));
const mockUseQueryBuilder = useQueryBuilder as jest.MockedFunction<
typeof useQueryBuilder
>;
const mockUseGetMetricUnits = useGetMetricUnits as jest.MockedFunction<
typeof useGetMetricUnits
>;
function createMockCurrentQuery(
queryType: EQueryType,
queryData: Query['builder']['queryData'] = [],
): Query {
return {
queryType,
promql: [],
builder: {
queryData,
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [],
id: 'test-id',
};
}
describe('useGetYAxisUnit', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return undefined yAxisUnit and not call useGetMetricUnits when currentQuery is null', async () => {
mockUseQueryBuilder.mockReturnValue(({
currentQuery: undefined,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: [],
isLoading: false,
isError: false,
metrics: [],
});
const { result } = renderHook(() => useGetYAxisUnit());
expect(result.current.yAxisUnit).toBeUndefined();
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(false);
expect(mockUseGetMetricUnits).toHaveBeenCalledWith([], false);
});
it('should return undefined yAxisUnit when queryType is PROM', async () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.PROM);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: [],
isLoading: false,
isError: false,
metrics: [],
});
const { result } = renderHook(() => useGetYAxisUnit());
expect(result.current.yAxisUnit).toBeUndefined();
expect(mockUseGetMetricUnits).toHaveBeenCalledWith([], false);
});
it('should return undefined yAxisUnit when queryType is CLICKHOUSE', async () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.CLICKHOUSE);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: [],
isLoading: false,
isError: false,
metrics: [],
});
const { result } = renderHook(() => useGetYAxisUnit());
expect(result.current.yAxisUnit).toBeUndefined();
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(false);
expect(mockUseGetMetricUnits).toHaveBeenCalledWith([], false);
});
it('should return undefined yAxisUnit when dataSource is TRACES', async () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.QUERY_BUILDER, [
{
dataSource: DataSource.TRACES,
aggregateAttribute: { key: 'trace_metric' },
} as Query['builder']['queryData'][0],
]);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: [],
isLoading: false,
isError: false,
metrics: [],
});
const { result } = renderHook(() => useGetYAxisUnit());
expect(result.current.yAxisUnit).toBeUndefined();
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(false);
expect(mockUseGetMetricUnits).toHaveBeenCalledWith([], false);
});
it('should return undefined yAxisUnit when dataSource is LOGS', async () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.QUERY_BUILDER, [
{
dataSource: DataSource.LOGS,
aggregateAttribute: { key: 'log_metric' },
} as Query['builder']['queryData'][number],
]);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: [],
isLoading: false,
isError: false,
metrics: [],
});
const { result } = renderHook(() => useGetYAxisUnit());
expect(result.current.yAxisUnit).toBeUndefined();
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(false);
expect(mockUseGetMetricUnits).toHaveBeenCalledWith([], false);
});
it('should extract all metric names from queryData', () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.QUERY_BUILDER, [
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric1' },
queryName: 'query1',
} as Query['builder']['queryData'][number],
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric2' },
queryName: 'query2',
} as Query['builder']['queryData'][number],
]);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: [],
isLoading: false,
isError: false,
metrics: [],
});
renderHook(() => useGetYAxisUnit());
expect(mockUseGetMetricUnits).toHaveBeenCalledWith(
['metric1', 'metric2'],
true,
);
});
it('should extract metric name for the selected query only', () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.QUERY_BUILDER, [
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric1' },
queryName: 'query1',
} as Query['builder']['queryData'][number],
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric2' },
queryName: 'query2',
} as Query['builder']['queryData'][number],
]);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: [],
isLoading: false,
isError: false,
metrics: [],
});
renderHook(() => useGetYAxisUnit('query2'));
expect(mockUseGetMetricUnits).toHaveBeenCalledWith(['metric2'], true);
});
it('should return undefined when units array is empty', async () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.QUERY_BUILDER, [
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric1' },
queryName: 'query1',
} as Query['builder']['queryData'][number],
]);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: [],
isLoading: false,
isError: false,
metrics: [],
});
const { result } = renderHook(() => useGetYAxisUnit());
expect(result.current.yAxisUnit).toBeUndefined();
});
it('should return the unit when there is a single non-empty unit', async () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.QUERY_BUILDER, [
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric1' },
queryName: 'query1',
} as Query['builder']['queryData'][number],
]);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: ['bytes'],
isLoading: false,
isError: false,
metrics: [],
});
const { result } = renderHook(() => useGetYAxisUnit());
expect(result.current.yAxisUnit).toBe('bytes');
});
it('should return undefined when there is a single empty unit', async () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.QUERY_BUILDER, [
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric1' },
queryName: 'query1',
} as Query['builder']['queryData'][number],
]);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: [''],
isLoading: false,
isError: false,
metrics: [],
});
const { result } = renderHook(() => useGetYAxisUnit());
expect(result.current.yAxisUnit).toBeUndefined();
});
it('should return the unit when all units are the same and non-empty', async () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.QUERY_BUILDER, [
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric1' },
queryName: 'query1',
} as Query['builder']['queryData'][number],
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric2' },
queryName: 'query2',
} as Query['builder']['queryData'][number],
]);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: ['bytes', 'bytes'],
isLoading: false,
isError: false,
metrics: [],
});
const { result } = renderHook(() => useGetYAxisUnit());
expect(result.current.yAxisUnit).toBe('bytes');
});
it('should return undefined when units are different', async () => {
const mockCurrentQuery = createMockCurrentQuery(EQueryType.QUERY_BUILDER, [
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric2' },
queryName: 'query2',
} as Query['builder']['queryData'][number],
{
dataSource: DataSource.METRICS,
aggregateAttribute: { key: 'metric3' },
queryName: 'query3',
} as Query['builder']['queryData'][number],
]);
mockUseQueryBuilder.mockReturnValue(({
currentQuery: mockCurrentQuery,
} as Partial<QueryBuilderContextType>) as QueryBuilderContextType);
mockUseGetMetricUnits.mockReturnValue({
units: ['bytes', 'seconds'],
isLoading: false,
isError: false,
metrics: [],
});
const { result } = renderHook(() => useGetYAxisUnit());
expect(result.current.yAxisUnit).toBeUndefined();
});
});

View File

@@ -0,0 +1,81 @@
import { useGetMetricUnits } from 'container/MetricsExplorer/Explorer/utils';
import { useEffect, useMemo, useState } from 'react';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { useQueryBuilder } from './queryBuilder/useQueryBuilder';
interface UseGetYAxisUnitResult {
yAxisUnit: string | undefined;
isLoading: boolean;
isError: boolean;
}
function useGetYAxisUnit(selectedQueryName?: string): UseGetYAxisUnitResult {
const { currentQuery } = useQueryBuilder();
const [yAxisUnit, setYAxisUnit] = useState<string | undefined>();
const metricNames: string[] | null = useMemo(() => {
if (currentQuery?.queryType !== EQueryType.QUERY_BUILDER) {
return null;
}
const dataSource = currentQuery?.builder?.queryData?.[0]?.dataSource;
if (dataSource !== DataSource.METRICS) {
return null;
}
// If a selected query name is provided, return the metric name for that query only
if (selectedQueryName) {
const currentMetricNames: string[] = [];
currentQuery?.builder?.queryData?.forEach((query) => {
if (
query.queryName === selectedQueryName &&
query.aggregateAttribute?.key
) {
currentMetricNames.push(query.aggregateAttribute?.key);
}
});
return currentMetricNames.length ? currentMetricNames : null;
}
const currentMetricNames: string[] = [];
currentQuery?.builder?.queryData?.forEach((query) => {
if (query.aggregateAttribute?.key) {
currentMetricNames.push(query.aggregateAttribute?.key);
}
});
return currentMetricNames.length ? currentMetricNames : null;
}, [
selectedQueryName,
currentQuery?.builder?.queryData,
currentQuery?.queryType,
]);
const { units, isLoading, isError } = useGetMetricUnits(
metricNames ?? [],
!!metricNames,
);
const areAllMetricUnitsSame = useMemo(
() => units.every((unit) => unit === units[0]),
[units],
);
useEffect(() => {
if (units.length === 0) {
setYAxisUnit(undefined);
} else if (units.length === 1 && units[0] !== '') {
setYAxisUnit(units[0]);
} else if (areAllMetricUnitsSame) {
if (units[0] !== '') {
setYAxisUnit(units[0]);
} else {
setYAxisUnit(undefined);
}
} else if (units.length > 1 && !areAllMetricUnitsSame) {
setYAxisUnit(undefined);
}
}, [units, areAllMetricUnitsSame]);
return { yAxisUnit, isLoading, isError };
}
export default useGetYAxisUnit;

View File

@@ -0,0 +1,15 @@
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
export interface MetricMetadata {
description: string;
type: MetricType;
unit: string;
temporality: Temporality;
isMonotonic: boolean;
}
export interface MetricMetadataResponse {
status: string;
data: MetricMetadata;
}