diff --git a/frontend/src/api/metricsExplorer/getRelatedMetrics.ts b/frontend/src/api/metricsExplorer/getRelatedMetrics.ts new file mode 100644 index 0000000000..2f4aa5dfa6 --- /dev/null +++ b/frontend/src/api/metricsExplorer/getRelatedMetrics.ts @@ -0,0 +1,60 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; + +export interface RelatedMetricsPayload { + start: number; + end: number; + currentMetricName: string; +} + +export interface RelatedMetricDashboard { + dashboard_name: string; + dashboard_id: string; + widget_id: string; + widget_name: string; +} + +export interface RelatedMetricAlert { + alert_name: string; + alert_id: string; +} + +export interface RelatedMetric { + name: string; + query: IBuilderQuery; + dashboards: RelatedMetricDashboard[]; + alerts: RelatedMetricAlert[]; +} + +export interface RelatedMetricsResponse { + status: 'success'; + data: { + related_metrics: RelatedMetric[]; + }; +} + +export const getRelatedMetrics = async ( + props: RelatedMetricsPayload, + signal?: AbortSignal, + headers?: Record, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/metrics/related', props, { + signal, + headers, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + params: props, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 85ad7edec4..0e4b8e0332 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -50,4 +50,5 @@ export const REACT_QUERY_KEY = { 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', + GET_RELATED_METRICS: 'GET_RELATED_METRICS', }; diff --git a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx index b6c12ec744..6e0e9c57bd 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx +++ b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx @@ -100,11 +100,19 @@ function ExplorerOptions({ const ref = useRef(null); const isDarkMode = useIsDarkMode(); const isLogsExplorer = sourcepage === DataSource.LOGS; + const isMetricsExplorer = sourcepage === DataSource.METRICS; const PRESERVED_VIEW_LOCAL_STORAGE_KEY = LOCALSTORAGE.LAST_USED_SAVED_VIEWS; - const PRESERVED_VIEW_TYPE = isLogsExplorer - ? PreservedViewsTypes.LOGS - : PreservedViewsTypes.TRACES; + + const PRESERVED_VIEW_TYPE = useMemo(() => { + if (isLogsExplorer) { + return PreservedViewsTypes.LOGS; + } + if (isMetricsExplorer) { + return PreservedViewsTypes.METRICS; + } + return PreservedViewsTypes.TRACES; + }, [isLogsExplorer, isMetricsExplorer]); const onModalToggle = useCallback((value: boolean) => { setIsExport(value); @@ -127,6 +135,10 @@ function ExplorerOptions({ logEvent('Logs Explorer: Save view clicked', { panelType, }); + } else if (isMetricsExplorer) { + logEvent('Metrics Explorer: Save view clicked', { + panelType, + }); } setIsSaveModalOpen(!isSaveModalOpen); }; @@ -161,6 +173,10 @@ function ExplorerOptions({ logEvent('Logs Explorer: Create alert', { panelType, }); + } else if (isMetricsExplorer) { + logEvent('Metrics Explorer: Create alert', { + panelType, + }); } const stringifiedQuery = handleConditionalQueryModification(); @@ -186,6 +202,10 @@ function ExplorerOptions({ logEvent('Logs Explorer: Add to dashboard clicked', { panelType, }); + } else if (isMetricsExplorer) { + logEvent('Metrics Explorer: Add to dashboard clicked', { + panelType, + }); } setIsExport(true); }; @@ -395,6 +415,11 @@ function ExplorerOptions({ panelType, viewName: option?.value, }); + } else if (isMetricsExplorer) { + logEvent('Metrics Explorer: Select view', { + panelType, + viewName: option?.value, + }); } updatePreservedViewInLocalStorage(option); @@ -491,6 +516,11 @@ function ExplorerOptions({ panelType, viewName: newViewName, }); + } else if (isMetricsExplorer) { + logEvent('Metrics Explorer: Save view successful', { + panelType, + viewName: newViewName, + }); } }; @@ -570,6 +600,27 @@ function ExplorerOptions({ viewsData?.data?.data, ]); + const infoIconText = useMemo(() => { + if (isLogsExplorer) { + return 'Learn more about Logs explorer'; + } + if (isMetricsExplorer) { + return 'Learn more about Metrics explorer'; + } + return 'Learn more about Traces explorer'; + }, [isLogsExplorer, isMetricsExplorer]); + + const infoIconLink = useMemo(() => { + if (isLogsExplorer) { + return 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar'; + } + // TODO: Add metrics explorer info icon link + if (isMetricsExplorer) { + return ''; + } + return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar'; + }, [isLogsExplorer, isMetricsExplorer]); + return (
{ @@ -693,28 +744,22 @@ function ExplorerOptions({
- - {isLogsExplorer - ? 'Learn more about Logs explorer ' - : 'Learn more about Traces explorer '} - - {' '} - here - {' '} -
- } - > - - + {/* Hide the info icon for metrics explorer until we get the docs link */} + {!isMetricsExplorer && ( + + {infoIconText} + + {' '} + here + {' '} + + } + > + + + )} + + +
+ {selectedTab === ExplorerTabs.TIME_SERIES && ( + + )} + {selectedTab === ExplorerTabs.RELATED_METRICS && ( + + )} +
+ + ); } diff --git a/frontend/src/container/MetricsExplorer/Explorer/QuerySection.tsx b/frontend/src/container/MetricsExplorer/Explorer/QuerySection.tsx new file mode 100644 index 0000000000..399bfcf091 --- /dev/null +++ b/frontend/src/container/MetricsExplorer/Explorer/QuerySection.tsx @@ -0,0 +1,32 @@ +import { Button } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { QueryBuilder } from 'container/QueryBuilder'; +import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles'; +import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { DataSource } from 'types/common/queryBuilder'; + +function QuerySection(): JSX.Element { + const { handleRunQuery } = useQueryBuilder(); + + const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES); + + return ( +
+ + + + } + /> +
+ ); +} + +export default QuerySection; diff --git a/frontend/src/container/MetricsExplorer/Explorer/RelatedMetrics.tsx b/frontend/src/container/MetricsExplorer/Explorer/RelatedMetrics.tsx new file mode 100644 index 0000000000..f5df0f3cd9 --- /dev/null +++ b/frontend/src/container/MetricsExplorer/Explorer/RelatedMetrics.tsx @@ -0,0 +1,167 @@ +import { Color } from '@signozhq/design-tokens'; +import { Card, Col, Input, Row, Select, Skeleton } from 'antd'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useResizeObserver } from 'hooks/useDimensions'; +import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; +import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { Gauge } from 'lucide-react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import RelatedMetricsCard from './RelatedMetricsCard'; +import { RelatedMetricsProps, RelatedMetricWithQueryResult } from './types'; +import { useGetRelatedMetricsGraphs } from './useGetRelatedMetricsGraphs'; + +function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element { + const isDarkMode = useIsDarkMode(); + const graphRef = useRef(null); + const dimensions = useResizeObserver(graphRef); + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + const [selectedMetricName, setSelectedMetricName] = useState( + null, + ); + const [selectedRelatedMetric, setSelectedRelatedMetric] = useState('all'); + const [searchValue, setSearchValue] = useState(null); + + const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [ + minTime, + ]); + const endMs = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [ + maxTime, + ]); + + useEffect(() => { + if (metricNames.length) { + setSelectedMetricName(metricNames[0]); + } + }, [metricNames]); + + const { relatedMetrics, isRelatedMetricsLoading } = useGetRelatedMetricsGraphs( + { + selectedMetricName, + startMs, + endMs, + }, + ); + + const metricNamesSelectOptions = useMemo( + () => + metricNames.map((name) => ({ + value: name, + label: name, + })), + [metricNames], + ); + + const relatedMetricsSelectOptions = useMemo(() => { + const options: { value: string; label: string }[] = [ + { + value: 'all', + label: 'All', + }, + ]; + relatedMetrics.forEach((metric) => { + options.push({ + value: metric.name, + label: metric.name, + }); + }); + return options; + }, [relatedMetrics]); + + const filteredRelatedMetrics = useMemo(() => { + let filteredMetrics: RelatedMetricWithQueryResult[] = []; + if (selectedRelatedMetric === 'all') { + filteredMetrics = [...relatedMetrics]; + } else { + filteredMetrics = relatedMetrics.filter( + (metric) => metric.name === selectedRelatedMetric, + ); + } + if (searchValue?.length) { + filteredMetrics = filteredMetrics.filter((metric) => + metric.name.toLowerCase().includes(searchValue?.toLowerCase() ?? ''), + ); + } + return filteredMetrics; + }, [relatedMetrics, selectedRelatedMetric, searchValue]); + + const chartData = useMemo( + () => + filteredRelatedMetrics.map(({ queryResult }) => + getUPlotChartData(queryResult.data?.payload), + ), + [filteredRelatedMetrics], + ); + + const options = useMemo( + () => + filteredRelatedMetrics.map(({ queryResult }) => + getUPlotChartOptions({ + apiResponse: queryResult.data?.payload, + isDarkMode, + dimensions, + yAxisUnit: '', + softMax: null, + softMin: null, + minTimeScale: startMs, + maxTimeScale: endMs, + }), + ), + [filteredRelatedMetrics, isDarkMode, dimensions, startMs, endMs], + ); + + return ( +
+
+ setSearchValue(e.target.value)} + bordered + addonBefore={ +