diff --git a/frontend/src/container/NewWidget/LeftContainer/index.tsx b/frontend/src/container/NewWidget/LeftContainer/index.tsx index 83d99aefcf..6b72e6a6ad 100644 --- a/frontend/src/container/NewWidget/LeftContainer/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/index.tsx @@ -6,7 +6,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useDashboard } from 'providers/Dashboard/Dashboard'; -import { memo } from 'react'; +import { memo, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { GlobalReducer } from 'types/reducer/globalTime'; @@ -27,6 +27,7 @@ function LeftContainer({ requestData, setRequestData, isLoadingPanelData, + setQueryResponse, }: WidgetGraphProps): JSX.Element { const { stagedQuery } = useQueryBuilder(); const { selectedDashboard } = useDashboard(); @@ -49,6 +50,13 @@ function LeftContainer({ }, ); + // Update parent component with query response for legend colors + useEffect(() => { + if (setQueryResponse) { + setQueryResponse(queryResponse); + } + }, [queryResponse, setQueryResponse]); + return ( <> (null); + const [isOverflowing, setIsOverflowing] = useState(false); + + useEffect(() => { + const checkOverflow = (): void => { + if (textRef.current) { + const isTextOverflowing = + textRef.current.scrollWidth > textRef.current.clientWidth; + setIsOverflowing(isTextOverflowing); + } + }; + + checkOverflow(); + // Check on window resize + window.addEventListener('resize', checkOverflow); + return (): void => window.removeEventListener('resize', checkOverflow); + }, [label]); + + return ( + + + {label} + + + ); +} + +interface LegendColorsProps { + customLegendColors: Record; + setCustomLegendColors: Dispatch>>; + queryResponse?: UseQueryResult< + SuccessResponse, + Error + >; +} + +function LegendColors({ + customLegendColors, + setCustomLegendColors, + queryResponse = null as any, +}: LegendColorsProps): JSX.Element { + const { currentQuery } = useQueryBuilder(); + const isDarkMode = useIsDarkMode(); + + // Get legend labels from query response or current query + const legendLabels = useMemo(() => { + if (queryResponse?.data?.payload?.data?.result) { + return queryResponse.data.payload.data.result.map((item: any) => + getLabelName(item.metric || {}, item.queryName || '', item.legend || ''), + ); + } + + // Fallback to query data if no response available + return currentQuery.builder.queryData.map((query) => + getLabelName({}, query.queryName || '', query.legend || ''), + ); + }, [queryResponse, currentQuery]); + + // Get current or default color for a legend + const getColorForLegend = (label: string): string => { + if (customLegendColors[label]) { + return customLegendColors[label]; + } + return generateColor( + label, + isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, + ); + }; + + // Handle color change + const handleColorChange = (label: string, color: string): void => { + setCustomLegendColors((prev) => ({ + ...prev, + [label]: color, + })); + }; + + // Reset to default color + const resetToDefault = (label: string): void => { + setCustomLegendColors((prev) => { + const updated = { ...prev }; + delete updated[label]; + return updated; + }); + }; + + // Reset all colors to default + const resetAllColors = (): void => { + setCustomLegendColors({}); + }; + + const items = [ + { + key: 'legend-colors', + label: ( +
+ + Legend Colors +
+ ), + children: ( +
+ {legendLabels.length === 0 ? ( + + No legends available. Run a query to see legend options. + + ) : ( + <> +
+ +
+
+ {legendLabels.map((label: string) => ( +
+ + handleColorChange(label, color.toHexString()) + } + size="small" + showText={false} + trigger="click" + > +
+
+
+ +
+ {customLegendColors[label] && ( +
+ { + e.stopPropagation(); + resetToDefault(label); + }} + > + Reset + +
+ )} +
+ +
+ ))} +
+ + )} +
+ ), + }, + ]; + + return ( +
+ +
+ ); +} + +LegendColors.defaultProps = { + queryResponse: null, +}; + +export default LegendColors; diff --git a/frontend/src/container/NewWidget/RightContainer/RightContainer.styles.scss b/frontend/src/container/NewWidget/RightContainer/RightContainer.styles.scss index a32fcb8850..1c3b78494a 100644 --- a/frontend/src/container/NewWidget/RightContainer/RightContainer.styles.scss +++ b/frontend/src/container/NewWidget/RightContainer/RightContainer.styles.scss @@ -174,6 +174,10 @@ gap: 8px; } + .legend-colors { + margin-top: 16px; + } + .panel-time-text { margin-top: 16px; color: var(--bg-vanilla-400); diff --git a/frontend/src/container/NewWidget/RightContainer/constants.ts b/frontend/src/container/NewWidget/RightContainer/constants.ts index 3735b684a5..53aa7eae99 100644 --- a/frontend/src/container/NewWidget/RightContainer/constants.ts +++ b/frontend/src/container/NewWidget/RightContainer/constants.ts @@ -164,3 +164,17 @@ export const panelTypeVsLegendPosition: { [PANEL_TYPES.HISTOGRAM]: false, [PANEL_TYPES.EMPTY_WIDGET]: false, } as const; + +export const panelTypeVsLegendColors: { + [key in PANEL_TYPES]: boolean; +} = { + [PANEL_TYPES.TIME_SERIES]: true, + [PANEL_TYPES.VALUE]: false, + [PANEL_TYPES.TABLE]: false, + [PANEL_TYPES.LIST]: false, + [PANEL_TYPES.PIE]: true, + [PANEL_TYPES.BAR]: true, + [PANEL_TYPES.TRACE]: false, + [PANEL_TYPES.HISTOGRAM]: true, + [PANEL_TYPES.EMPTY_WIDGET]: false, +} as const; diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx index ac7f0fede5..f0e518ab06 100644 --- a/frontend/src/container/NewWidget/RightContainer/index.tsx +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -30,11 +30,14 @@ import { useRef, useState, } from 'react'; +import { UseQueryResult } from 'react-query'; +import { SuccessResponse } from 'types/api'; import { ColumnUnit, LegendPosition, Widgets, } from 'types/api/dashboard/getAll'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { DataSource } from 'types/common/queryBuilder'; import { popupContainer } from 'utils/selectPopupContainer'; @@ -44,6 +47,7 @@ import { panelTypeVsColumnUnitPreferences, panelTypeVsCreateAlert, panelTypeVsFillSpan, + panelTypeVsLegendColors, panelTypeVsLegendPosition, panelTypeVsLogScale, panelTypeVsPanelTimePreferences, @@ -52,6 +56,7 @@ import { panelTypeVsThreshold, panelTypeVsYAxisUnit, } from './constants'; +import LegendColors from './LegendColors/LegendColors'; import ThresholdSelector from './Threshold/ThresholdSelector'; import { ThresholdProps } from './Threshold/types'; import { timePreferance } from './timeItems'; @@ -105,6 +110,9 @@ function RightContainer({ setIsLogScale, legendPosition, setLegendPosition, + customLegendColors, + setCustomLegendColors, + queryResponse, }: RightContainerProps): JSX.Element { const { selectedDashboard } = useDashboard(); const [inputValue, setInputValue] = useState(title); @@ -136,6 +144,7 @@ function RightContainer({ const allowPanelTimePreference = panelTypeVsPanelTimePreferences[selectedGraph]; const allowLegendPosition = panelTypeVsLegendPosition[selectedGraph]; + const allowLegendColors = panelTypeVsLegendColors[selectedGraph]; const allowPanelColumnPreference = panelTypeVsColumnUnitPreferences[selectedGraph]; @@ -462,6 +471,16 @@ function RightContainer({ )} + + {allowLegendColors && ( +
+ +
+ )} {allowCreateAlerts && ( @@ -529,10 +548,17 @@ interface RightContainerProps { setIsLogScale: Dispatch>; legendPosition: LegendPosition; setLegendPosition: Dispatch>; + customLegendColors: Record; + setCustomLegendColors: Dispatch>>; + queryResponse?: UseQueryResult< + SuccessResponse, + Error + >; } RightContainer.defaultProps = { selectedWidget: undefined, + queryResponse: null, }; export default RightContainer; diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 06d73f5751..af6b3cda35 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -34,9 +34,11 @@ import { } from 'providers/Dashboard/util'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; import { generatePath, useParams } from 'react-router-dom'; import { AppState } from 'store/reducers'; +import { SuccessResponse } from 'types/api'; import { ColumnUnit, Dashboard, @@ -44,6 +46,7 @@ import { Widgets, } from 'types/api/dashboard/getAll'; import { IField } from 'types/api/logs/fields'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { EQueryType } from 'types/common/dashboard'; import { DataSource } from 'types/common/queryBuilder'; import { GlobalReducer } from 'types/reducer/globalTime'; @@ -191,6 +194,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { const [legendPosition, setLegendPosition] = useState( selectedWidget?.legendPosition || LegendPosition.BOTTOM, ); + const [customLegendColors, setCustomLegendColors] = useState< + Record + >(selectedWidget?.customLegendColors || {}); + const [saveModal, setSaveModal] = useState(false); const [discardModal, setDiscardModal] = useState(false); @@ -257,6 +264,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { selectedTracesFields, isLogScale, legendPosition, + customLegendColors, columnWidths: columnWidths?.[selectedWidget?.id], }; }); @@ -282,6 +290,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { stackedBarChart, isLogScale, legendPosition, + customLegendColors, columnWidths, ]); @@ -340,6 +349,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { // hence while changing the query contains the older value and the processing logic fails const [isLoadingPanelData, setIsLoadingPanelData] = useState(false); + // State to hold query response for sharing between left and right containers + const [queryResponse, setQueryResponse] = useState< + UseQueryResult, Error> + >(null as any); + // request data should be handled by the parent and the child components should consume the same // this has been moved here from the left container const [requestData, setRequestData] = useState(() => { @@ -482,6 +496,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { selectedLogFields: selectedWidget?.selectedLogFields || [], selectedTracesFields: selectedWidget?.selectedTracesFields || [], legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM, + customLegendColors: selectedWidget?.customLegendColors || {}, }, ] : [ @@ -510,6 +525,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { selectedLogFields: selectedWidget?.selectedLogFields || [], selectedTracesFields: selectedWidget?.selectedTracesFields || [], legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM, + customLegendColors: selectedWidget?.customLegendColors || {}, }, ...afterWidgets, ], @@ -723,6 +739,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { requestData={requestData} setRequestData={setRequestData} isLoadingPanelData={isLoadingPanelData} + setQueryResponse={setQueryResponse} /> )} @@ -766,6 +783,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element { setIsLogScale={setIsLogScale} legendPosition={legendPosition} setLegendPosition={setLegendPosition} + customLegendColors={customLegendColors} + setCustomLegendColors={setCustomLegendColors} + queryResponse={queryResponse} softMin={softMin} setSoftMin={setSoftMin} softMax={softMax} diff --git a/frontend/src/container/NewWidget/types.ts b/frontend/src/container/NewWidget/types.ts index c3952e935a..0b9b001e7c 100644 --- a/frontend/src/container/NewWidget/types.ts +++ b/frontend/src/container/NewWidget/types.ts @@ -27,6 +27,11 @@ export interface WidgetGraphProps { requestData: GetQueryResultsProps; setRequestData: Dispatch>; isLoadingPanelData: boolean; + setQueryResponse?: Dispatch< + SetStateAction< + UseQueryResult, Error> + > + >; } export type WidgetGraphContainerProps = { diff --git a/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx b/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx index 948f62af3f..237331dcda 100644 --- a/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/PiePanelWrapper.tsx @@ -50,14 +50,19 @@ function PiePanelWrapper({ color: string; }[] = [].concat( ...(panelData - .map((d) => ({ - label: getLabelName(d.metric, d.queryName || '', d.legend || ''), - value: d.values?.[0]?.[1], - color: generateColor( - getLabelName(d.metric, d.queryName || '', d.legend || ''), - isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, - ), - })) + .map((d) => { + const label = getLabelName(d.metric, d.queryName || '', d.legend || ''); + return { + label, + value: d.values?.[0]?.[1], + color: + widget?.customLegendColors?.[label] || + generateColor( + label, + isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, + ), + }; + }) .filter((d) => d !== undefined) as never[]), ); diff --git a/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx b/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx index 67099b48bd..1f50bc9eb1 100644 --- a/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/UplotPanelWrapper.tsx @@ -138,6 +138,7 @@ function UplotPanelWrapper({ timezone: timezone.value, customSeries, isLogScale: widget?.isLogScale, + colorMapping: widget?.customLegendColors, enhancedLegend: true, // Enable enhanced legend legendPosition: widget?.legendPosition, }), @@ -166,6 +167,7 @@ function UplotPanelWrapper({ customSeries, widget?.isLogScale, widget?.legendPosition, + widget?.customLegendColors, ], ); diff --git a/frontend/src/lib/uPlotLib/utils/getSeriesData.ts b/frontend/src/lib/uPlotLib/utils/getSeriesData.ts index 5de1f6d207..2c72acb6d6 100644 --- a/frontend/src/lib/uPlotLib/utils/getSeriesData.ts +++ b/frontend/src/lib/uPlotLib/utils/getSeriesData.ts @@ -34,6 +34,7 @@ const getSeries = ({ panelType, hiddenGraph, isDarkMode, + colorMapping, }: GetSeriesProps): uPlot.Options['series'] => { const configurations: uPlot.Series[] = [ { label: 'Timestamp', stroke: 'purple' }, @@ -52,10 +53,12 @@ const getSeries = ({ legend || '', ); - const color = generateColor( - label, - isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, - ); + const color = + colorMapping?.[label] || + generateColor( + label, + isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, + ); const pointSize = seriesList[i].values.length > 1 ? 5 : 10; const showPoints = !(seriesList[i].values.length > 1); @@ -105,6 +108,7 @@ export type GetSeriesProps = { hiddenGraph?: { [key: string]: boolean; }; + colorMapping?: Record; }; export default getSeries; diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index d9bfb1af43..2e6d883287 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -117,6 +117,7 @@ export interface IBaseWidget { isLogScale?: boolean; columnWidths?: Record; legendPosition?: LegendPosition; + customLegendColors?: Record; } export interface Widgets extends IBaseWidget { query: Query;