mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 21:18:58 +08:00
feat: allow custom color pallete in panel for legends (#8063)
This commit is contained in:
parent
aaeffae1bd
commit
8990fb7a73
@ -6,7 +6,7 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
|||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { memo } from 'react';
|
import { memo, useEffect } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@ -27,6 +27,7 @@ function LeftContainer({
|
|||||||
requestData,
|
requestData,
|
||||||
setRequestData,
|
setRequestData,
|
||||||
isLoadingPanelData,
|
isLoadingPanelData,
|
||||||
|
setQueryResponse,
|
||||||
}: WidgetGraphProps): JSX.Element {
|
}: WidgetGraphProps): JSX.Element {
|
||||||
const { stagedQuery } = useQueryBuilder();
|
const { stagedQuery } = useQueryBuilder();
|
||||||
const { selectedDashboard } = useDashboard();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<WidgetGraph
|
<WidgetGraph
|
||||||
|
@ -0,0 +1,169 @@
|
|||||||
|
.legend-colors-container {
|
||||||
|
.legend-colors-collapse {
|
||||||
|
.ant-collapse-header {
|
||||||
|
padding: 8px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-collapse-content-box {
|
||||||
|
padding: 0 0 12px 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-colors-content {
|
||||||
|
.legend-colors-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-top: 4px;
|
||||||
|
|
||||||
|
/* Webkit scrollbar styling */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
border-radius: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--bg-slate-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox scrollbar styling */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--bg-slate-400) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item-wrapper {
|
||||||
|
.ant-color-picker-trigger {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr max-content;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-slate-400);
|
||||||
|
border-color: var(--bg-slate-500);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.legend-marker {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-label-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.reset-link {
|
||||||
|
font-size: 11px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-colors-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.legend-colors-container {
|
||||||
|
.legend-colors-content {
|
||||||
|
.legend-items {
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--bg-vanilla-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollbar-color: var(--bg-vanilla-400) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-slate-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,205 @@
|
|||||||
|
import './LegendColors.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Collapse, ColorPicker, Tooltip, Typography } from 'antd';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import getLabelName from 'lib/getLabelName';
|
||||||
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
|
import { Palette } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
// Component for legend text with conditional tooltip
|
||||||
|
function LegendText({ label }: { label: string }): JSX.Element {
|
||||||
|
const textRef = useRef<HTMLSpanElement>(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 (
|
||||||
|
<Tooltip title={label} open={isOverflowing ? undefined : false}>
|
||||||
|
<span ref={textRef} className="legend-label-text">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LegendColorsProps {
|
||||||
|
customLegendColors: Record<string, string>;
|
||||||
|
setCustomLegendColors: Dispatch<SetStateAction<Record<string, string>>>;
|
||||||
|
queryResponse?: UseQueryResult<
|
||||||
|
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||||
|
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: (
|
||||||
|
<section className="legend-colors-header">
|
||||||
|
<Palette size={16} />
|
||||||
|
<Typography.Text className="typography">Legend Colors</Typography.Text>
|
||||||
|
</section>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<div className="legend-colors-content">
|
||||||
|
{legendLabels.length === 0 ? (
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
No legends available. Run a query to see legend options.
|
||||||
|
</Typography.Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="legend-colors-header">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
onClick={resetAllColors}
|
||||||
|
disabled={Object.keys(customLegendColors).length === 0}
|
||||||
|
>
|
||||||
|
Reset All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="legend-items">
|
||||||
|
{legendLabels.map((label: string) => (
|
||||||
|
<div key={label} className="legend-item-wrapper">
|
||||||
|
<ColorPicker
|
||||||
|
value={getColorForLegend(label)}
|
||||||
|
onChange={(color): void =>
|
||||||
|
handleColorChange(label, color.toHexString())
|
||||||
|
}
|
||||||
|
size="small"
|
||||||
|
showText={false}
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
<div className="legend-item">
|
||||||
|
<div className="legend-info">
|
||||||
|
<div
|
||||||
|
className="legend-marker"
|
||||||
|
style={{ backgroundColor: getColorForLegend(label) }}
|
||||||
|
/>
|
||||||
|
<LegendText label={label} />
|
||||||
|
</div>
|
||||||
|
{customLegendColors[label] && (
|
||||||
|
<div className="legend-actions">
|
||||||
|
<Typography.Link
|
||||||
|
className="reset-link"
|
||||||
|
onClick={(e): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
resetToDefault(label);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Typography.Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ColorPicker>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="legend-colors-container">
|
||||||
|
<Collapse
|
||||||
|
items={items}
|
||||||
|
ghost
|
||||||
|
size="small"
|
||||||
|
expandIconPosition="end"
|
||||||
|
className="legend-colors-collapse"
|
||||||
|
accordion
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LegendColors.defaultProps = {
|
||||||
|
queryResponse: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LegendColors;
|
@ -174,6 +174,10 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.legend-colors {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-time-text {
|
.panel-time-text {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
|
@ -164,3 +164,17 @@ export const panelTypeVsLegendPosition: {
|
|||||||
[PANEL_TYPES.HISTOGRAM]: false,
|
[PANEL_TYPES.HISTOGRAM]: false,
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||||
} as const;
|
} 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;
|
||||||
|
@ -30,11 +30,14 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
ColumnUnit,
|
ColumnUnit,
|
||||||
LegendPosition,
|
LegendPosition,
|
||||||
Widgets,
|
Widgets,
|
||||||
} from 'types/api/dashboard/getAll';
|
} from 'types/api/dashboard/getAll';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
@ -44,6 +47,7 @@ import {
|
|||||||
panelTypeVsColumnUnitPreferences,
|
panelTypeVsColumnUnitPreferences,
|
||||||
panelTypeVsCreateAlert,
|
panelTypeVsCreateAlert,
|
||||||
panelTypeVsFillSpan,
|
panelTypeVsFillSpan,
|
||||||
|
panelTypeVsLegendColors,
|
||||||
panelTypeVsLegendPosition,
|
panelTypeVsLegendPosition,
|
||||||
panelTypeVsLogScale,
|
panelTypeVsLogScale,
|
||||||
panelTypeVsPanelTimePreferences,
|
panelTypeVsPanelTimePreferences,
|
||||||
@ -52,6 +56,7 @@ import {
|
|||||||
panelTypeVsThreshold,
|
panelTypeVsThreshold,
|
||||||
panelTypeVsYAxisUnit,
|
panelTypeVsYAxisUnit,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
import LegendColors from './LegendColors/LegendColors';
|
||||||
import ThresholdSelector from './Threshold/ThresholdSelector';
|
import ThresholdSelector from './Threshold/ThresholdSelector';
|
||||||
import { ThresholdProps } from './Threshold/types';
|
import { ThresholdProps } from './Threshold/types';
|
||||||
import { timePreferance } from './timeItems';
|
import { timePreferance } from './timeItems';
|
||||||
@ -105,6 +110,9 @@ function RightContainer({
|
|||||||
setIsLogScale,
|
setIsLogScale,
|
||||||
legendPosition,
|
legendPosition,
|
||||||
setLegendPosition,
|
setLegendPosition,
|
||||||
|
customLegendColors,
|
||||||
|
setCustomLegendColors,
|
||||||
|
queryResponse,
|
||||||
}: RightContainerProps): JSX.Element {
|
}: RightContainerProps): JSX.Element {
|
||||||
const { selectedDashboard } = useDashboard();
|
const { selectedDashboard } = useDashboard();
|
||||||
const [inputValue, setInputValue] = useState(title);
|
const [inputValue, setInputValue] = useState(title);
|
||||||
@ -136,6 +144,7 @@ function RightContainer({
|
|||||||
const allowPanelTimePreference =
|
const allowPanelTimePreference =
|
||||||
panelTypeVsPanelTimePreferences[selectedGraph];
|
panelTypeVsPanelTimePreferences[selectedGraph];
|
||||||
const allowLegendPosition = panelTypeVsLegendPosition[selectedGraph];
|
const allowLegendPosition = panelTypeVsLegendPosition[selectedGraph];
|
||||||
|
const allowLegendColors = panelTypeVsLegendColors[selectedGraph];
|
||||||
|
|
||||||
const allowPanelColumnPreference =
|
const allowPanelColumnPreference =
|
||||||
panelTypeVsColumnUnitPreferences[selectedGraph];
|
panelTypeVsColumnUnitPreferences[selectedGraph];
|
||||||
@ -462,6 +471,16 @@ function RightContainer({
|
|||||||
</Select>
|
</Select>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{allowLegendColors && (
|
||||||
|
<section className="legend-colors">
|
||||||
|
<LegendColors
|
||||||
|
customLegendColors={customLegendColors}
|
||||||
|
setCustomLegendColors={setCustomLegendColors}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{allowCreateAlerts && (
|
{allowCreateAlerts && (
|
||||||
@ -529,10 +548,17 @@ interface RightContainerProps {
|
|||||||
setIsLogScale: Dispatch<SetStateAction<boolean>>;
|
setIsLogScale: Dispatch<SetStateAction<boolean>>;
|
||||||
legendPosition: LegendPosition;
|
legendPosition: LegendPosition;
|
||||||
setLegendPosition: Dispatch<SetStateAction<LegendPosition>>;
|
setLegendPosition: Dispatch<SetStateAction<LegendPosition>>;
|
||||||
|
customLegendColors: Record<string, string>;
|
||||||
|
setCustomLegendColors: Dispatch<SetStateAction<Record<string, string>>>;
|
||||||
|
queryResponse?: UseQueryResult<
|
||||||
|
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||||
|
Error
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
RightContainer.defaultProps = {
|
RightContainer.defaultProps = {
|
||||||
selectedWidget: undefined,
|
selectedWidget: undefined,
|
||||||
|
queryResponse: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RightContainer;
|
export default RightContainer;
|
||||||
|
@ -34,9 +34,11 @@ import {
|
|||||||
} from 'providers/Dashboard/util';
|
} from 'providers/Dashboard/util';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { generatePath, useParams } from 'react-router-dom';
|
import { generatePath, useParams } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
ColumnUnit,
|
ColumnUnit,
|
||||||
Dashboard,
|
Dashboard,
|
||||||
@ -44,6 +46,7 @@ import {
|
|||||||
Widgets,
|
Widgets,
|
||||||
} from 'types/api/dashboard/getAll';
|
} from 'types/api/dashboard/getAll';
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@ -191,6 +194,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
const [legendPosition, setLegendPosition] = useState<LegendPosition>(
|
const [legendPosition, setLegendPosition] = useState<LegendPosition>(
|
||||||
selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
||||||
);
|
);
|
||||||
|
const [customLegendColors, setCustomLegendColors] = useState<
|
||||||
|
Record<string, string>
|
||||||
|
>(selectedWidget?.customLegendColors || {});
|
||||||
|
|
||||||
const [saveModal, setSaveModal] = useState(false);
|
const [saveModal, setSaveModal] = useState(false);
|
||||||
const [discardModal, setDiscardModal] = useState(false);
|
const [discardModal, setDiscardModal] = useState(false);
|
||||||
|
|
||||||
@ -257,6 +264,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
selectedTracesFields,
|
selectedTracesFields,
|
||||||
isLogScale,
|
isLogScale,
|
||||||
legendPosition,
|
legendPosition,
|
||||||
|
customLegendColors,
|
||||||
columnWidths: columnWidths?.[selectedWidget?.id],
|
columnWidths: columnWidths?.[selectedWidget?.id],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -282,6 +290,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
stackedBarChart,
|
stackedBarChart,
|
||||||
isLogScale,
|
isLogScale,
|
||||||
legendPosition,
|
legendPosition,
|
||||||
|
customLegendColors,
|
||||||
columnWidths,
|
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
|
// hence while changing the query contains the older value and the processing logic fails
|
||||||
const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false);
|
const [isLoadingPanelData, setIsLoadingPanelData] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// State to hold query response for sharing between left and right containers
|
||||||
|
const [queryResponse, setQueryResponse] = useState<
|
||||||
|
UseQueryResult<SuccessResponse<MetricRangePayloadProps, unknown>, Error>
|
||||||
|
>(null as any);
|
||||||
|
|
||||||
// request data should be handled by the parent and the child components should consume the same
|
// 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
|
// this has been moved here from the left container
|
||||||
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
|
||||||
@ -482,6 +496,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
selectedLogFields: selectedWidget?.selectedLogFields || [],
|
selectedLogFields: selectedWidget?.selectedLogFields || [],
|
||||||
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
||||||
legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
||||||
|
customLegendColors: selectedWidget?.customLegendColors || {},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
@ -510,6 +525,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
selectedLogFields: selectedWidget?.selectedLogFields || [],
|
selectedLogFields: selectedWidget?.selectedLogFields || [],
|
||||||
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
selectedTracesFields: selectedWidget?.selectedTracesFields || [],
|
||||||
legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
legendPosition: selectedWidget?.legendPosition || LegendPosition.BOTTOM,
|
||||||
|
customLegendColors: selectedWidget?.customLegendColors || {},
|
||||||
},
|
},
|
||||||
...afterWidgets,
|
...afterWidgets,
|
||||||
],
|
],
|
||||||
@ -723,6 +739,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
requestData={requestData}
|
requestData={requestData}
|
||||||
setRequestData={setRequestData}
|
setRequestData={setRequestData}
|
||||||
isLoadingPanelData={isLoadingPanelData}
|
isLoadingPanelData={isLoadingPanelData}
|
||||||
|
setQueryResponse={setQueryResponse}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</OverlayScrollbar>
|
</OverlayScrollbar>
|
||||||
@ -766,6 +783,9 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
|||||||
setIsLogScale={setIsLogScale}
|
setIsLogScale={setIsLogScale}
|
||||||
legendPosition={legendPosition}
|
legendPosition={legendPosition}
|
||||||
setLegendPosition={setLegendPosition}
|
setLegendPosition={setLegendPosition}
|
||||||
|
customLegendColors={customLegendColors}
|
||||||
|
setCustomLegendColors={setCustomLegendColors}
|
||||||
|
queryResponse={queryResponse}
|
||||||
softMin={softMin}
|
softMin={softMin}
|
||||||
setSoftMin={setSoftMin}
|
setSoftMin={setSoftMin}
|
||||||
softMax={softMax}
|
softMax={softMax}
|
||||||
|
@ -27,6 +27,11 @@ export interface WidgetGraphProps {
|
|||||||
requestData: GetQueryResultsProps;
|
requestData: GetQueryResultsProps;
|
||||||
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||||
isLoadingPanelData: boolean;
|
isLoadingPanelData: boolean;
|
||||||
|
setQueryResponse?: Dispatch<
|
||||||
|
SetStateAction<
|
||||||
|
UseQueryResult<SuccessResponse<MetricRangePayloadProps, unknown>, Error>
|
||||||
|
>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WidgetGraphContainerProps = {
|
export type WidgetGraphContainerProps = {
|
||||||
|
@ -50,14 +50,19 @@ function PiePanelWrapper({
|
|||||||
color: string;
|
color: string;
|
||||||
}[] = [].concat(
|
}[] = [].concat(
|
||||||
...(panelData
|
...(panelData
|
||||||
.map((d) => ({
|
.map((d) => {
|
||||||
label: getLabelName(d.metric, d.queryName || '', d.legend || ''),
|
const label = getLabelName(d.metric, d.queryName || '', d.legend || '');
|
||||||
|
return {
|
||||||
|
label,
|
||||||
value: d.values?.[0]?.[1],
|
value: d.values?.[0]?.[1],
|
||||||
color: generateColor(
|
color:
|
||||||
getLabelName(d.metric, d.queryName || '', d.legend || ''),
|
widget?.customLegendColors?.[label] ||
|
||||||
|
generateColor(
|
||||||
|
label,
|
||||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||||
),
|
),
|
||||||
}))
|
};
|
||||||
|
})
|
||||||
.filter((d) => d !== undefined) as never[]),
|
.filter((d) => d !== undefined) as never[]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -138,6 +138,7 @@ function UplotPanelWrapper({
|
|||||||
timezone: timezone.value,
|
timezone: timezone.value,
|
||||||
customSeries,
|
customSeries,
|
||||||
isLogScale: widget?.isLogScale,
|
isLogScale: widget?.isLogScale,
|
||||||
|
colorMapping: widget?.customLegendColors,
|
||||||
enhancedLegend: true, // Enable enhanced legend
|
enhancedLegend: true, // Enable enhanced legend
|
||||||
legendPosition: widget?.legendPosition,
|
legendPosition: widget?.legendPosition,
|
||||||
}),
|
}),
|
||||||
@ -166,6 +167,7 @@ function UplotPanelWrapper({
|
|||||||
customSeries,
|
customSeries,
|
||||||
widget?.isLogScale,
|
widget?.isLogScale,
|
||||||
widget?.legendPosition,
|
widget?.legendPosition,
|
||||||
|
widget?.customLegendColors,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ const getSeries = ({
|
|||||||
panelType,
|
panelType,
|
||||||
hiddenGraph,
|
hiddenGraph,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
|
colorMapping,
|
||||||
}: GetSeriesProps): uPlot.Options['series'] => {
|
}: GetSeriesProps): uPlot.Options['series'] => {
|
||||||
const configurations: uPlot.Series[] = [
|
const configurations: uPlot.Series[] = [
|
||||||
{ label: 'Timestamp', stroke: 'purple' },
|
{ label: 'Timestamp', stroke: 'purple' },
|
||||||
@ -52,7 +53,9 @@ const getSeries = ({
|
|||||||
legend || '',
|
legend || '',
|
||||||
);
|
);
|
||||||
|
|
||||||
const color = generateColor(
|
const color =
|
||||||
|
colorMapping?.[label] ||
|
||||||
|
generateColor(
|
||||||
label,
|
label,
|
||||||
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||||
);
|
);
|
||||||
@ -105,6 +108,7 @@ export type GetSeriesProps = {
|
|||||||
hiddenGraph?: {
|
hiddenGraph?: {
|
||||||
[key: string]: boolean;
|
[key: string]: boolean;
|
||||||
};
|
};
|
||||||
|
colorMapping?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getSeries;
|
export default getSeries;
|
||||||
|
@ -117,6 +117,7 @@ export interface IBaseWidget {
|
|||||||
isLogScale?: boolean;
|
isLogScale?: boolean;
|
||||||
columnWidths?: Record<string, number>;
|
columnWidths?: Record<string, number>;
|
||||||
legendPosition?: LegendPosition;
|
legendPosition?: LegendPosition;
|
||||||
|
customLegendColors?: Record<string, string>;
|
||||||
}
|
}
|
||||||
export interface Widgets extends IBaseWidget {
|
export interface Widgets extends IBaseWidget {
|
||||||
query: Query;
|
query: Query;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user