mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-04 11:25:52 +08:00
Improved graph panel full view (#3039)
* feat: done with prd full view * refactor: updated some variable and naming convection * feat: when click on label only select associated graph * feat: made the table scrollable * feat: update the table column length * feat: save notification after saving state * refactor: removed unwanted code * refactor: renamed some file * fix: linter issue * fix: position of save button * refactor: seperated widgetGraphComponent from gridGraphComponent * feat: fetching the localstorage data while initial loading of graph * fix: dependency of graphVisibilityHandler for other component * refactor: updated the notification msg on save * fix: linter error * refactor: remove the update logic of graph from graph component * refactor: created utils and move some utility code * refactor: place the checkbox component in fullview * refactor: updated the utils function added enun localstorage * refactor: added enum for table columns data * refactor: name changes to graphVisibilityStates * refactor: shifted the type to types.ts * refactor: sepearated the type from graph componnet * refactor: seperated graphOptions from graph component * refactor: updated imports * refactor: shifted the logic to utils * refactor: remove unused file and check for full view * refactor: using PanelType instead of GraphType * refactor: changed the variable name * refactor: provided checks of useEffect * test: added unit test case for utility function * refactor: one on one maping of props and value * refactor: panelTypeAndGraphManagerVisibility as a props * refactor: remove the enforing of type in useChartMutable * refactor: updated the test case * refactor: moved types to types.ts files * refactor: separated types from components * refactor: one to one mapping and cancel feature * refactor: remove unwanted useEffect and used eventEmitter * fix: only open chart visibility will change issue * refactor: removed unwanted useEffect * refactor: resolve the hang issue for full view * refactor: legend to checkbox connection, separated code * refactor: updated styled component GraphContainer * chore: removed unwanted consoles * refactor: ux changes * fix: eslint and updated test case * refactor: review comments * chore: fix types * refactor: made utils for getIsGraphLegendToggleAvailable * refactor: removed the ref mutation from graphPanelSwitch * refactor: resolve the issue of chart state not getting reflect outside fullview * refactor: common utility for toggle graphs visibility in chart * refactor: shifted ref to perticular component level * test: removed extra space * chore: close on save and NaN infinity check * refactor: added yAxisUnit to GraphManager table header * refactor: create a function for appending yAxisUnit to table header * fix: decimal upto 2 decimal points --------- Co-authored-by: Vishal Sharma <makeavish786@gmail.com> Co-authored-by: Pranay Prateek <pranay@signoz.io> Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
parent
668f0c6e2b
commit
b339f0509b
@ -54,6 +54,7 @@
|
|||||||
"dompurify": "3.0.0",
|
"dompurify": "3.0.0",
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "8.2.0",
|
||||||
"event-source-polyfill": "1.0.31",
|
"event-source-polyfill": "1.0.31",
|
||||||
|
"eventemitter3": "5.0.1",
|
||||||
"file-loader": "6.1.1",
|
"file-loader": "6.1.1",
|
||||||
"fontfaceobserver": "2.3.0",
|
"fontfaceobserver": "2.3.0",
|
||||||
"history": "4.10.1",
|
"history": "4.10.1",
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Chart, ChartType, Plugin } from 'chart.js';
|
import { Chart, ChartType, Plugin } from 'chart.js';
|
||||||
|
import { Events } from 'constants/events';
|
||||||
import { colors } from 'lib/getRandomColor';
|
import { colors } from 'lib/getRandomColor';
|
||||||
import { get } from 'lodash-es';
|
import { get } from 'lodash-es';
|
||||||
|
import { eventEmitter } from 'utils/getEventEmitter';
|
||||||
|
|
||||||
const getOrCreateLegendList = (
|
const getOrCreateLegendList = (
|
||||||
chart: Chart,
|
chart: Chart,
|
||||||
@ -74,6 +76,10 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
|
|||||||
item.datasetIndex,
|
item.datasetIndex,
|
||||||
!chart.isDatasetVisible(item.datasetIndex),
|
!chart.isDatasetVisible(item.datasetIndex),
|
||||||
);
|
);
|
||||||
|
eventEmitter.emit(Events.UPDATE_GRAPH_MANAGER_TABLE, {
|
||||||
|
name: id,
|
||||||
|
index: item.datasetIndex,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
chart.update();
|
chart.update();
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
ActiveElement,
|
|
||||||
BarController,
|
BarController,
|
||||||
BarElement,
|
BarElement,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
Chart,
|
Chart,
|
||||||
ChartData,
|
|
||||||
ChartEvent,
|
|
||||||
ChartOptions,
|
|
||||||
ChartType,
|
ChartType,
|
||||||
Decimation,
|
Decimation,
|
||||||
Filler,
|
Filler,
|
||||||
@ -21,33 +17,28 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
import {
|
||||||
|
forwardRef,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { hasData } from './hasData';
|
import { hasData } from './hasData';
|
||||||
import { getAxisLabelColor } from './helpers';
|
|
||||||
import { legend } from './Plugin';
|
import { legend } from './Plugin';
|
||||||
import {
|
import { createDragSelectPlugin } from './Plugin/DragSelect';
|
||||||
createDragSelectPlugin,
|
|
||||||
createDragSelectPluginOptions,
|
|
||||||
dragSelectPluginId,
|
|
||||||
DragSelectPluginOptions,
|
|
||||||
} from './Plugin/DragSelect';
|
|
||||||
import { emptyGraph } from './Plugin/EmptyGraph';
|
import { emptyGraph } from './Plugin/EmptyGraph';
|
||||||
import {
|
import { createIntersectionCursorPlugin } from './Plugin/IntersectionCursor';
|
||||||
createIntersectionCursorPlugin,
|
|
||||||
createIntersectionCursorPluginOptions,
|
|
||||||
intersectionCursorPluginId,
|
|
||||||
IntersectionCursorPluginOptions,
|
|
||||||
} from './Plugin/IntersectionCursor';
|
|
||||||
import { TooltipPosition as TooltipPositionHandler } from './Plugin/Tooltip';
|
import { TooltipPosition as TooltipPositionHandler } from './Plugin/Tooltip';
|
||||||
import { LegendsContainer } from './styles';
|
import { LegendsContainer } from './styles';
|
||||||
|
import { CustomChartOptions, GraphProps, ToggleGraphProps } from './types';
|
||||||
|
import { getGraphOptions, toggleGraph } from './utils';
|
||||||
import { useXAxisTimeUnit } from './xAxisConfig';
|
import { useXAxisTimeUnit } from './xAxisConfig';
|
||||||
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
|
||||||
|
|
||||||
Chart.register(
|
Chart.register(
|
||||||
LineElement,
|
LineElement,
|
||||||
@ -70,265 +61,125 @@ Chart.register(
|
|||||||
|
|
||||||
Tooltip.positioners.custom = TooltipPositionHandler;
|
Tooltip.positioners.custom = TooltipPositionHandler;
|
||||||
|
|
||||||
function Graph({
|
const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
||||||
animate = true,
|
(
|
||||||
data,
|
{
|
||||||
type,
|
animate = true,
|
||||||
title,
|
data,
|
||||||
isStacked,
|
type,
|
||||||
onClickHandler,
|
title,
|
||||||
name,
|
isStacked,
|
||||||
yAxisUnit = 'short',
|
onClickHandler,
|
||||||
forceReRender,
|
name,
|
||||||
staticLine,
|
yAxisUnit = 'short',
|
||||||
containerHeight,
|
forceReRender,
|
||||||
onDragSelect,
|
staticLine,
|
||||||
dragSelectColor,
|
containerHeight,
|
||||||
}: GraphProps): JSX.Element {
|
onDragSelect,
|
||||||
const nearestDatasetIndex = useRef<null | number>(null);
|
dragSelectColor,
|
||||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
},
|
||||||
const isDarkMode = useIsDarkMode();
|
ref,
|
||||||
|
): JSX.Element => {
|
||||||
|
const nearestDatasetIndex = useRef<null | number>(null);
|
||||||
|
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const currentTheme = isDarkMode ? 'dark' : 'light';
|
const currentTheme = isDarkMode ? 'dark' : 'light';
|
||||||
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
||||||
|
|
||||||
const lineChartRef = useRef<Chart>();
|
const lineChartRef = useRef<Chart>();
|
||||||
const getGridColor = useCallback(() => {
|
|
||||||
if (currentTheme === undefined) {
|
|
||||||
return 'rgba(231,233,237,0.1)';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTheme === 'dark') {
|
useImperativeHandle(
|
||||||
return 'rgba(231,233,237,0.1)';
|
ref,
|
||||||
}
|
(): ToggleGraphProps => ({
|
||||||
|
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||||
return 'rgba(231,233,237,0.8)';
|
toggleGraph(graphIndex, isVisible, lineChartRef);
|
||||||
}, [currentTheme]);
|
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
const buildChart = useCallback(() => {
|
|
||||||
if (lineChartRef.current !== undefined) {
|
|
||||||
lineChartRef.current.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chartRef.current !== null) {
|
|
||||||
const options: CustomChartOptions = {
|
|
||||||
animation: {
|
|
||||||
duration: animate ? 200 : 0,
|
|
||||||
},
|
},
|
||||||
responsive: true,
|
}),
|
||||||
maintainAspectRatio: false,
|
);
|
||||||
interaction: {
|
|
||||||
mode: 'index',
|
|
||||||
intersect: false,
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
annotation: staticLine
|
|
||||||
? {
|
|
||||||
annotations: [
|
|
||||||
{
|
|
||||||
type: 'line',
|
|
||||||
yMin: staticLine.yMin,
|
|
||||||
yMax: staticLine.yMax,
|
|
||||||
borderColor: staticLine.borderColor,
|
|
||||||
borderWidth: staticLine.borderWidth,
|
|
||||||
label: {
|
|
||||||
content: staticLine.lineText,
|
|
||||||
enabled: true,
|
|
||||||
font: {
|
|
||||||
size: 10,
|
|
||||||
},
|
|
||||||
borderWidth: 0,
|
|
||||||
position: 'start',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
color: staticLine.textColor,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
title: {
|
|
||||||
display: title !== undefined,
|
|
||||||
text: title,
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
title(context) {
|
|
||||||
const date = dayjs(context[0].parsed.x);
|
|
||||||
return date.format('MMM DD, YYYY, HH:mm:ss');
|
|
||||||
},
|
|
||||||
label(context) {
|
|
||||||
let label = context.dataset.label || '';
|
|
||||||
|
|
||||||
if (label) {
|
const getGridColor = useCallback(() => {
|
||||||
label += ': ';
|
if (currentTheme === undefined) {
|
||||||
}
|
return 'rgba(231,233,237,0.1)';
|
||||||
if (context.parsed.y !== null) {
|
|
||||||
label += getToolTipValue(context.parsed.y.toString(), yAxisUnit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return label;
|
|
||||||
},
|
|
||||||
labelTextColor(labelData) {
|
|
||||||
if (labelData.datasetIndex === nearestDatasetIndex.current) {
|
|
||||||
return 'rgba(255, 255, 255, 1)';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'rgba(255, 255, 255, 0.75)';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
position: 'custom',
|
|
||||||
itemSort(item1, item2) {
|
|
||||||
return item2.parsed.y - item1.parsed.y;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[dragSelectPluginId]: createDragSelectPluginOptions(
|
|
||||||
!!onDragSelect,
|
|
||||||
onDragSelect,
|
|
||||||
dragSelectColor,
|
|
||||||
),
|
|
||||||
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
|
|
||||||
!!onDragSelect,
|
|
||||||
currentTheme === 'dark' ? 'white' : 'black',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
grid: {
|
|
||||||
display: true,
|
|
||||||
color: getGridColor(),
|
|
||||||
drawTicks: true,
|
|
||||||
},
|
|
||||||
adapters: {
|
|
||||||
date: chartjsAdapter,
|
|
||||||
},
|
|
||||||
time: {
|
|
||||||
unit: xAxisTimeUnit?.unitName || 'minute',
|
|
||||||
stepSize: xAxisTimeUnit?.stepSize || 1,
|
|
||||||
displayFormats: {
|
|
||||||
millisecond: 'HH:mm:ss',
|
|
||||||
second: 'HH:mm:ss',
|
|
||||||
minute: 'HH:mm',
|
|
||||||
hour: 'MM/dd HH:mm',
|
|
||||||
day: 'MM/dd',
|
|
||||||
week: 'MM/dd',
|
|
||||||
month: 'yy-MM',
|
|
||||||
year: 'yy',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: 'time',
|
|
||||||
ticks: { color: getAxisLabelColor(currentTheme) },
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
display: true,
|
|
||||||
grid: {
|
|
||||||
display: true,
|
|
||||||
color: getGridColor(),
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: getAxisLabelColor(currentTheme),
|
|
||||||
// Include a dollar sign in the ticks
|
|
||||||
callback(value) {
|
|
||||||
return getYAxisFormattedValue(value.toString(), yAxisUnit);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stacked: {
|
|
||||||
display: isStacked === undefined ? false : 'auto',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
elements: {
|
|
||||||
line: {
|
|
||||||
tension: 0,
|
|
||||||
cubicInterpolationMode: 'monotone',
|
|
||||||
},
|
|
||||||
point: {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
hoverBackgroundColor: (ctx: any) => {
|
|
||||||
if (ctx?.element?.options?.borderColor) {
|
|
||||||
return ctx.element.options.borderColor;
|
|
||||||
}
|
|
||||||
return 'rgba(0,0,0,0.1)';
|
|
||||||
},
|
|
||||||
hoverRadius: 5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onClick: (event, element, chart) => {
|
|
||||||
if (onClickHandler) {
|
|
||||||
onClickHandler(event, element, chart, data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onHover: (event, _, chart) => {
|
|
||||||
if (event.native) {
|
|
||||||
const interactions = chart.getElementsAtEventForMode(
|
|
||||||
event.native,
|
|
||||||
'nearest',
|
|
||||||
{
|
|
||||||
intersect: false,
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (interactions[0]) {
|
|
||||||
nearestDatasetIndex.current = interactions[0].datasetIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartHasData = hasData(data);
|
|
||||||
const chartPlugins = [];
|
|
||||||
|
|
||||||
if (chartHasData) {
|
|
||||||
chartPlugins.push(createIntersectionCursorPlugin());
|
|
||||||
chartPlugins.push(createDragSelectPlugin());
|
|
||||||
chartPlugins.push(legend(name, data.datasets.length > 3));
|
|
||||||
} else {
|
|
||||||
chartPlugins.push(emptyGraph);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lineChartRef.current = new Chart(chartRef.current, {
|
if (currentTheme === 'dark') {
|
||||||
type,
|
return 'rgba(231,233,237,0.1)';
|
||||||
data,
|
}
|
||||||
options,
|
|
||||||
plugins: chartPlugins,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
animate,
|
|
||||||
title,
|
|
||||||
getGridColor,
|
|
||||||
xAxisTimeUnit?.unitName,
|
|
||||||
xAxisTimeUnit?.stepSize,
|
|
||||||
isStacked,
|
|
||||||
type,
|
|
||||||
data,
|
|
||||||
name,
|
|
||||||
yAxisUnit,
|
|
||||||
onClickHandler,
|
|
||||||
staticLine,
|
|
||||||
onDragSelect,
|
|
||||||
dragSelectColor,
|
|
||||||
currentTheme,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
return 'rgba(231,233,237,0.8)';
|
||||||
buildChart();
|
}, [currentTheme]);
|
||||||
}, [buildChart, forceReRender]);
|
|
||||||
|
|
||||||
return (
|
const buildChart = useCallback(() => {
|
||||||
<div style={{ height: containerHeight }}>
|
if (lineChartRef.current !== undefined) {
|
||||||
<canvas ref={chartRef} />
|
lineChartRef.current.destroy();
|
||||||
<LegendsContainer id={name} />
|
}
|
||||||
</div>
|
|
||||||
);
|
if (chartRef.current !== null) {
|
||||||
}
|
const options: CustomChartOptions = getGraphOptions(
|
||||||
|
animate,
|
||||||
|
staticLine,
|
||||||
|
title,
|
||||||
|
nearestDatasetIndex,
|
||||||
|
yAxisUnit,
|
||||||
|
onDragSelect,
|
||||||
|
dragSelectColor,
|
||||||
|
currentTheme,
|
||||||
|
getGridColor,
|
||||||
|
xAxisTimeUnit,
|
||||||
|
isStacked,
|
||||||
|
onClickHandler,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartHasData = hasData(data);
|
||||||
|
const chartPlugins = [];
|
||||||
|
|
||||||
|
if (chartHasData) {
|
||||||
|
chartPlugins.push(createIntersectionCursorPlugin());
|
||||||
|
chartPlugins.push(createDragSelectPlugin());
|
||||||
|
} else {
|
||||||
|
chartPlugins.push(emptyGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
chartPlugins.push(legend(name, data.datasets.length > 3));
|
||||||
|
|
||||||
|
lineChartRef.current = new Chart(chartRef.current, {
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
options,
|
||||||
|
plugins: chartPlugins,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
animate,
|
||||||
|
staticLine,
|
||||||
|
title,
|
||||||
|
yAxisUnit,
|
||||||
|
onDragSelect,
|
||||||
|
dragSelectColor,
|
||||||
|
currentTheme,
|
||||||
|
getGridColor,
|
||||||
|
xAxisTimeUnit,
|
||||||
|
isStacked,
|
||||||
|
onClickHandler,
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
buildChart();
|
||||||
|
}, [buildChart, forceReRender]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: containerHeight }}>
|
||||||
|
<canvas ref={chartRef} />
|
||||||
|
<LegendsContainer id={name} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
declare module 'chart.js' {
|
declare module 'chart.js' {
|
||||||
interface TooltipPositionerMap {
|
interface TooltipPositionerMap {
|
||||||
@ -336,45 +187,6 @@ declare module 'chart.js' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomChartOptions = ChartOptions & {
|
|
||||||
plugins: {
|
|
||||||
[dragSelectPluginId]: DragSelectPluginOptions | false;
|
|
||||||
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface GraphProps {
|
|
||||||
animate?: boolean;
|
|
||||||
type: ChartType;
|
|
||||||
data: Chart['data'];
|
|
||||||
title?: string;
|
|
||||||
isStacked?: boolean;
|
|
||||||
onClickHandler?: GraphOnClickHandler;
|
|
||||||
name: string;
|
|
||||||
yAxisUnit?: string;
|
|
||||||
forceReRender?: boolean | null | number;
|
|
||||||
staticLine?: StaticLineProps | undefined;
|
|
||||||
containerHeight?: string | number;
|
|
||||||
onDragSelect?: (start: number, end: number) => void;
|
|
||||||
dragSelectColor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StaticLineProps {
|
|
||||||
yMin: number | undefined;
|
|
||||||
yMax: number | undefined;
|
|
||||||
borderColor: string;
|
|
||||||
borderWidth: number;
|
|
||||||
lineText: string;
|
|
||||||
textColor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GraphOnClickHandler = (
|
|
||||||
event: ChartEvent,
|
|
||||||
elements: ActiveElement[],
|
|
||||||
chart: Chart,
|
|
||||||
data: ChartData,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
Graph.defaultProps = {
|
Graph.defaultProps = {
|
||||||
animate: undefined,
|
animate: undefined,
|
||||||
title: undefined,
|
title: undefined,
|
||||||
@ -388,6 +200,8 @@ Graph.defaultProps = {
|
|||||||
dragSelectColor: undefined,
|
dragSelectColor: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Graph.displayName = 'Graph';
|
||||||
|
|
||||||
export default memo(Graph, (prevProps, nextProps) =>
|
export default memo(Graph, (prevProps, nextProps) =>
|
||||||
isEqual(prevProps.data, nextProps.data),
|
isEqual(prevProps.data, nextProps.data),
|
||||||
);
|
);
|
||||||
|
78
frontend/src/components/Graph/types.ts
Normal file
78
frontend/src/components/Graph/types.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
ActiveElement,
|
||||||
|
Chart,
|
||||||
|
ChartData,
|
||||||
|
ChartEvent,
|
||||||
|
ChartOptions,
|
||||||
|
ChartType,
|
||||||
|
TimeUnit,
|
||||||
|
} from 'chart.js';
|
||||||
|
import { ForwardedRef } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
dragSelectPluginId,
|
||||||
|
DragSelectPluginOptions,
|
||||||
|
} from './Plugin/DragSelect';
|
||||||
|
import {
|
||||||
|
intersectionCursorPluginId,
|
||||||
|
IntersectionCursorPluginOptions,
|
||||||
|
} from './Plugin/IntersectionCursor';
|
||||||
|
|
||||||
|
export interface StaticLineProps {
|
||||||
|
yMin: number | undefined;
|
||||||
|
yMax: number | undefined;
|
||||||
|
borderColor: string;
|
||||||
|
borderWidth: number;
|
||||||
|
lineText: string;
|
||||||
|
textColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GraphOnClickHandler = (
|
||||||
|
event: ChartEvent,
|
||||||
|
elements: ActiveElement[],
|
||||||
|
chart: Chart,
|
||||||
|
data: ChartData,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type ToggleGraphProps = {
|
||||||
|
toggleGraph(graphIndex: number, isVisible: boolean): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CustomChartOptions = ChartOptions & {
|
||||||
|
plugins: {
|
||||||
|
[dragSelectPluginId]: DragSelectPluginOptions | false;
|
||||||
|
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GraphProps {
|
||||||
|
animate?: boolean;
|
||||||
|
type: ChartType;
|
||||||
|
data: Chart['data'];
|
||||||
|
title?: string;
|
||||||
|
isStacked?: boolean;
|
||||||
|
onClickHandler?: GraphOnClickHandler;
|
||||||
|
name: string;
|
||||||
|
yAxisUnit?: string;
|
||||||
|
forceReRender?: boolean | null | number;
|
||||||
|
staticLine?: StaticLineProps | undefined;
|
||||||
|
containerHeight?: string | number;
|
||||||
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
|
dragSelectColor?: string;
|
||||||
|
ref?: ForwardedRef<ToggleGraphProps | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAxisTimeUintConfig {
|
||||||
|
unitName: TimeUnit;
|
||||||
|
multiplier: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAxisTimeConfig {
|
||||||
|
unitName: TimeUnit;
|
||||||
|
stepSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITimeRange {
|
||||||
|
minTime: number | null;
|
||||||
|
maxTime: number | null;
|
||||||
|
}
|
223
frontend/src/components/Graph/utils.ts
Normal file
223
frontend/src/components/Graph/utils.ts
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
|
||||||
|
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { MutableRefObject } from 'react';
|
||||||
|
|
||||||
|
import { getAxisLabelColor } from './helpers';
|
||||||
|
import {
|
||||||
|
createDragSelectPluginOptions,
|
||||||
|
dragSelectPluginId,
|
||||||
|
} from './Plugin/DragSelect';
|
||||||
|
import {
|
||||||
|
createIntersectionCursorPluginOptions,
|
||||||
|
intersectionCursorPluginId,
|
||||||
|
} from './Plugin/IntersectionCursor';
|
||||||
|
import {
|
||||||
|
CustomChartOptions,
|
||||||
|
GraphOnClickHandler,
|
||||||
|
IAxisTimeConfig,
|
||||||
|
StaticLineProps,
|
||||||
|
} from './types';
|
||||||
|
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
||||||
|
|
||||||
|
export const toggleGraph = (
|
||||||
|
graphIndex: number,
|
||||||
|
isVisible: boolean,
|
||||||
|
lineChartRef: MutableRefObject<Chart | undefined>,
|
||||||
|
): void => {
|
||||||
|
if (lineChartRef && lineChartRef.current) {
|
||||||
|
const { type } = lineChartRef.current?.config as ChartConfiguration;
|
||||||
|
if (type === 'pie' || type === 'doughnut') {
|
||||||
|
lineChartRef.current?.toggleDataVisibility(graphIndex);
|
||||||
|
} else {
|
||||||
|
lineChartRef.current?.setDatasetVisibility(graphIndex, isVisible);
|
||||||
|
}
|
||||||
|
lineChartRef.current?.update();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getGraphOptions = (
|
||||||
|
animate: boolean,
|
||||||
|
staticLine: StaticLineProps | undefined,
|
||||||
|
title: string | undefined,
|
||||||
|
nearestDatasetIndex: MutableRefObject<number | null>,
|
||||||
|
yAxisUnit: string,
|
||||||
|
onDragSelect: ((start: number, end: number) => void) | undefined,
|
||||||
|
dragSelectColor: string | undefined,
|
||||||
|
currentTheme: 'dark' | 'light',
|
||||||
|
getGridColor: () => 'rgba(231,233,237,0.1)' | 'rgba(231,233,237,0.8)',
|
||||||
|
xAxisTimeUnit: IAxisTimeConfig,
|
||||||
|
isStacked: boolean | undefined,
|
||||||
|
onClickHandler: GraphOnClickHandler | undefined,
|
||||||
|
data: ChartData,
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
): CustomChartOptions => ({
|
||||||
|
animation: {
|
||||||
|
duration: animate ? 200 : 0,
|
||||||
|
},
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
annotation: staticLine
|
||||||
|
? {
|
||||||
|
annotations: [
|
||||||
|
{
|
||||||
|
type: 'line',
|
||||||
|
yMin: staticLine.yMin,
|
||||||
|
yMax: staticLine.yMax,
|
||||||
|
borderColor: staticLine.borderColor,
|
||||||
|
borderWidth: staticLine.borderWidth,
|
||||||
|
label: {
|
||||||
|
content: staticLine.lineText,
|
||||||
|
enabled: true,
|
||||||
|
font: {
|
||||||
|
size: 10,
|
||||||
|
},
|
||||||
|
borderWidth: 0,
|
||||||
|
position: 'start',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: staticLine.textColor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
title: {
|
||||||
|
display: title !== undefined,
|
||||||
|
text: title,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
title(context): string | string[] {
|
||||||
|
const date = dayjs(context[0].parsed.x);
|
||||||
|
return date.format('MMM DD, YYYY, HH:mm:ss');
|
||||||
|
},
|
||||||
|
label(context): string | string[] {
|
||||||
|
let label = context.dataset.label || '';
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
label += ': ';
|
||||||
|
}
|
||||||
|
if (context.parsed.y !== null) {
|
||||||
|
label += getToolTipValue(context.parsed.y.toString(), yAxisUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
},
|
||||||
|
labelTextColor(labelData): Color {
|
||||||
|
if (labelData.datasetIndex === nearestDatasetIndex.current) {
|
||||||
|
return 'rgba(255, 255, 255, 1)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'rgba(255, 255, 255, 0.75)';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
position: 'custom',
|
||||||
|
itemSort(item1, item2): number {
|
||||||
|
return item2.parsed.y - item1.parsed.y;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[dragSelectPluginId]: createDragSelectPluginOptions(
|
||||||
|
!!onDragSelect,
|
||||||
|
onDragSelect,
|
||||||
|
dragSelectColor,
|
||||||
|
),
|
||||||
|
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
|
||||||
|
!!onDragSelect,
|
||||||
|
currentTheme === 'dark' ? 'white' : 'black',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
color: getGridColor(),
|
||||||
|
drawTicks: true,
|
||||||
|
},
|
||||||
|
adapters: {
|
||||||
|
date: chartjsAdapter,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
unit: xAxisTimeUnit?.unitName || 'minute',
|
||||||
|
stepSize: xAxisTimeUnit?.stepSize || 1,
|
||||||
|
displayFormats: {
|
||||||
|
millisecond: 'HH:mm:ss',
|
||||||
|
second: 'HH:mm:ss',
|
||||||
|
minute: 'HH:mm',
|
||||||
|
hour: 'MM/dd HH:mm',
|
||||||
|
day: 'MM/dd',
|
||||||
|
week: 'MM/dd',
|
||||||
|
month: 'yy-MM',
|
||||||
|
year: 'yy',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'time',
|
||||||
|
ticks: { color: getAxisLabelColor(currentTheme) },
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: true,
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
color: getGridColor(),
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
color: getAxisLabelColor(currentTheme),
|
||||||
|
// Include a dollar sign in the ticks
|
||||||
|
callback(value): string {
|
||||||
|
return getYAxisFormattedValue(value.toString(), yAxisUnit);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stacked: {
|
||||||
|
display: isStacked === undefined ? false : 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
line: {
|
||||||
|
tension: 0,
|
||||||
|
cubicInterpolationMode: 'monotone',
|
||||||
|
},
|
||||||
|
point: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
hoverBackgroundColor: (ctx: any): string => {
|
||||||
|
if (ctx?.element?.options?.borderColor) {
|
||||||
|
return ctx.element.options.borderColor;
|
||||||
|
}
|
||||||
|
return 'rgba(0,0,0,0.1)';
|
||||||
|
},
|
||||||
|
hoverRadius: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onClick: (event, element, chart): void => {
|
||||||
|
if (onClickHandler) {
|
||||||
|
onClickHandler(event, element, chart, data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onHover: (event, _, chart): void => {
|
||||||
|
if (event.native) {
|
||||||
|
const interactions = chart.getElementsAtEventForMode(
|
||||||
|
event.native,
|
||||||
|
'nearest',
|
||||||
|
{
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (interactions[0]) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
nearestDatasetIndex.current = interactions[0].datasetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
@ -4,20 +4,7 @@ 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';
|
||||||
|
|
||||||
interface IAxisTimeUintConfig {
|
import { IAxisTimeConfig, IAxisTimeUintConfig, ITimeRange } from './types';
|
||||||
unitName: TimeUnit;
|
|
||||||
multiplier: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IAxisTimeConfig {
|
|
||||||
unitName: TimeUnit;
|
|
||||||
stepSize: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ITimeRange {
|
|
||||||
minTime: number | null;
|
|
||||||
maxTime: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TIME_UNITS: Record<TimeUnit, TimeUnit> = {
|
export const TIME_UNITS: Record<TimeUnit, TimeUnit> = {
|
||||||
millisecond: 'millisecond',
|
millisecond: 'millisecond',
|
||||||
|
4
frontend/src/constants/events.ts
Normal file
4
frontend/src/constants/events.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum Events {
|
||||||
|
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
|
||||||
|
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
||||||
|
}
|
@ -8,6 +8,7 @@ export enum LOCALSTORAGE {
|
|||||||
LOGS_LINES_PER_ROW = 'LOGS_LINES_PER_ROW',
|
LOGS_LINES_PER_ROW = 'LOGS_LINES_PER_ROW',
|
||||||
LOGS_LIST_OPTIONS = 'LOGS_LIST_OPTIONS',
|
LOGS_LIST_OPTIONS = 'LOGS_LIST_OPTIONS',
|
||||||
TRACES_LIST_OPTIONS = 'TRACES_LIST_OPTIONS',
|
TRACES_LIST_OPTIONS = 'TRACES_LIST_OPTIONS',
|
||||||
|
GRAPH_VISIBILITY_STATES = 'GRAPH_VISIBILITY_STATES',
|
||||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { StaticLineProps } from 'components/Graph';
|
import { StaticLineProps } from 'components/Graph/types';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
|
@ -0,0 +1,207 @@
|
|||||||
|
import { Button, Input } from 'antd';
|
||||||
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
|
import { Events } from 'constants/events';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { eventEmitter } from 'utils/getEventEmitter';
|
||||||
|
|
||||||
|
import { getGraphVisibilityStateOnDataChange } from '../utils';
|
||||||
|
import {
|
||||||
|
FilterTableAndSaveContainer,
|
||||||
|
FilterTableContainer,
|
||||||
|
SaveCancelButtonContainer,
|
||||||
|
SaveContainer,
|
||||||
|
} from './styles';
|
||||||
|
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
|
||||||
|
import { ExtendedChartDataset, GraphManagerProps } from './types';
|
||||||
|
import {
|
||||||
|
getDefaultTableDataSet,
|
||||||
|
saveLegendEntriesToLocalStorage,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function GraphManager({
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
yAxisUnit,
|
||||||
|
onToggleModelHandler,
|
||||||
|
}: GraphManagerProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
graphVisibilityStates: localstoredVisibilityStates,
|
||||||
|
legendEntry,
|
||||||
|
} = useMemo(
|
||||||
|
() =>
|
||||||
|
getGraphVisibilityStateOnDataChange({
|
||||||
|
data,
|
||||||
|
isExpandedName: false,
|
||||||
|
name,
|
||||||
|
}),
|
||||||
|
[data, name],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [graphVisibilityState, setGraphVisibilityState] = useState<boolean[]>(
|
||||||
|
localstoredVisibilityStates,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(
|
||||||
|
getDefaultTableDataSet(data),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
// useEffect for updating graph visibility state on data change
|
||||||
|
useEffect(() => {
|
||||||
|
const newGraphVisibilityStates = Array<boolean>(data.datasets.length).fill(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
data.datasets.forEach((dataset, i) => {
|
||||||
|
const index = legendEntry.findIndex(
|
||||||
|
(entry) => entry.label === dataset.label,
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
newGraphVisibilityStates[i] = legendEntry[index].show;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||||
|
name,
|
||||||
|
graphVisibilityStates: newGraphVisibilityStates,
|
||||||
|
});
|
||||||
|
setGraphVisibilityState(newGraphVisibilityStates);
|
||||||
|
}, [data, name, legendEntry]);
|
||||||
|
|
||||||
|
// useEffect for listening to events event graph legend is clicked
|
||||||
|
useEffect(() => {
|
||||||
|
const eventListener = eventEmitter.on(
|
||||||
|
Events.UPDATE_GRAPH_MANAGER_TABLE,
|
||||||
|
(data) => {
|
||||||
|
if (data.name === name) {
|
||||||
|
const newGraphVisibilityStates = graphVisibilityState;
|
||||||
|
newGraphVisibilityStates[data.index] = !newGraphVisibilityStates[
|
||||||
|
data.index
|
||||||
|
];
|
||||||
|
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||||
|
name,
|
||||||
|
graphVisibilityStates: newGraphVisibilityStates,
|
||||||
|
});
|
||||||
|
setGraphVisibilityState([...newGraphVisibilityStates]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return (): void => {
|
||||||
|
eventListener.off(Events.UPDATE_GRAPH_MANAGER_TABLE);
|
||||||
|
};
|
||||||
|
}, [graphVisibilityState, name]);
|
||||||
|
|
||||||
|
const checkBoxOnChangeHandler = useCallback(
|
||||||
|
(e: CheckboxChangeEvent, index: number): void => {
|
||||||
|
graphVisibilityState[index] = e.target.checked;
|
||||||
|
setGraphVisibilityState([...graphVisibilityState]);
|
||||||
|
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||||
|
name,
|
||||||
|
graphVisibilityStates: [...graphVisibilityState],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[graphVisibilityState, name],
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelClickedHandler = useCallback(
|
||||||
|
(labelIndex: number): void => {
|
||||||
|
const newGraphVisibilityStates = Array<boolean>(data.datasets.length).fill(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
newGraphVisibilityStates[labelIndex] = true;
|
||||||
|
setGraphVisibilityState([...newGraphVisibilityStates]);
|
||||||
|
eventEmitter.emit(Events.UPDATE_GRAPH_VISIBILITY_STATE, {
|
||||||
|
name,
|
||||||
|
graphVisibilityStates: newGraphVisibilityStates,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[data.datasets.length, name],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() =>
|
||||||
|
getGraphManagerTableColumns({
|
||||||
|
data,
|
||||||
|
checkBoxOnChangeHandler,
|
||||||
|
graphVisibilityState,
|
||||||
|
labelClickedHandler,
|
||||||
|
yAxisUnit,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
checkBoxOnChangeHandler,
|
||||||
|
data,
|
||||||
|
graphVisibilityState,
|
||||||
|
labelClickedHandler,
|
||||||
|
yAxisUnit,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterHandler = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const value = event.target.value.toString().toLowerCase();
|
||||||
|
const updatedDataSet = tableDataSet.map((item) => {
|
||||||
|
if (item.label?.toLocaleLowerCase().includes(value)) {
|
||||||
|
return { ...item, show: true };
|
||||||
|
}
|
||||||
|
return { ...item, show: false };
|
||||||
|
});
|
||||||
|
setTableDataSet(updatedDataSet);
|
||||||
|
},
|
||||||
|
[tableDataSet],
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveHandler = useCallback((): void => {
|
||||||
|
saveLegendEntriesToLocalStorage({
|
||||||
|
data,
|
||||||
|
graphVisibilityState,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
notifications.success({
|
||||||
|
message: 'The updated graphs & legends are saved',
|
||||||
|
});
|
||||||
|
if (onToggleModelHandler) {
|
||||||
|
onToggleModelHandler();
|
||||||
|
}
|
||||||
|
}, [data, graphVisibilityState, name, notifications, onToggleModelHandler]);
|
||||||
|
|
||||||
|
const dataSource = tableDataSet.filter((item) => item.show);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterTableAndSaveContainer>
|
||||||
|
<FilterTableContainer>
|
||||||
|
<Input onChange={filterHandler} placeholder="Filter Series" />
|
||||||
|
<ResizeTable
|
||||||
|
columns={columns}
|
||||||
|
dataSource={dataSource}
|
||||||
|
rowKey="index"
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ y: 240 }}
|
||||||
|
/>
|
||||||
|
</FilterTableContainer>
|
||||||
|
<SaveContainer>
|
||||||
|
<SaveCancelButtonContainer>
|
||||||
|
<Button type="default" onClick={onToggleModelHandler}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</SaveCancelButtonContainer>
|
||||||
|
<SaveCancelButtonContainer>
|
||||||
|
<Button onClick={saveHandler} type="primary">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</SaveCancelButtonContainer>
|
||||||
|
</SaveContainer>
|
||||||
|
</FilterTableAndSaveContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GraphManager.defaultProps = {
|
||||||
|
graphVisibilityStateHandler: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(
|
||||||
|
GraphManager,
|
||||||
|
(prevProps, nextProps) =>
|
||||||
|
isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name,
|
||||||
|
);
|
@ -0,0 +1,33 @@
|
|||||||
|
import { Checkbox, ConfigProvider } from 'antd';
|
||||||
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
|
|
||||||
|
import { CheckBoxProps } from '../types';
|
||||||
|
|
||||||
|
function CustomCheckBox({
|
||||||
|
data,
|
||||||
|
index,
|
||||||
|
graphVisibilityState,
|
||||||
|
checkBoxOnChangeHandler,
|
||||||
|
}: CheckBoxProps): JSX.Element {
|
||||||
|
const { datasets } = data;
|
||||||
|
|
||||||
|
const onChangeHandler = (e: CheckboxChangeEvent): void => {
|
||||||
|
checkBoxOnChangeHandler(e, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
token: {
|
||||||
|
colorPrimary: datasets[index].borderColor?.toString(),
|
||||||
|
colorBorder: datasets[index].borderColor?.toString(),
|
||||||
|
colorBgContainer: datasets[index].borderColor?.toString(),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox onChange={onChangeHandler} checked={graphVisibilityState[index]} />
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomCheckBox;
|
@ -0,0 +1,27 @@
|
|||||||
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
|
import { ColumnType } from 'antd/es/table';
|
||||||
|
import { ChartData } from 'chart.js';
|
||||||
|
|
||||||
|
import { DataSetProps } from '../types';
|
||||||
|
import CustomCheckBox from './CustomCheckBox';
|
||||||
|
|
||||||
|
export const getCheckBox = ({
|
||||||
|
data,
|
||||||
|
checkBoxOnChangeHandler,
|
||||||
|
graphVisibilityState,
|
||||||
|
}: GetCheckBoxProps): ColumnType<DataSetProps> => ({
|
||||||
|
render: (index: number): JSX.Element => (
|
||||||
|
<CustomCheckBox
|
||||||
|
data={data}
|
||||||
|
index={index}
|
||||||
|
checkBoxOnChangeHandler={checkBoxOnChangeHandler}
|
||||||
|
graphVisibilityState={graphVisibilityState}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface GetCheckBoxProps {
|
||||||
|
data: ChartData;
|
||||||
|
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||||
|
graphVisibilityState: boolean[];
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { ColumnType } from 'antd/es/table';
|
||||||
|
|
||||||
|
import { DataSetProps } from '../types';
|
||||||
|
import Label from './Label';
|
||||||
|
|
||||||
|
export const getLabel = (
|
||||||
|
labelClickedHandler: (labelIndex: number) => void,
|
||||||
|
): ColumnType<DataSetProps> => ({
|
||||||
|
render: (label: string, _, index): JSX.Element => (
|
||||||
|
<Label
|
||||||
|
label={label}
|
||||||
|
labelIndex={index}
|
||||||
|
labelClickedHandler={labelClickedHandler}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
@ -0,0 +1,80 @@
|
|||||||
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
|
import { ColumnType } from 'antd/es/table';
|
||||||
|
import { ChartData } from 'chart.js';
|
||||||
|
|
||||||
|
import { ColumnsKeyAndDataIndex, ColumnsTitle } from '../contants';
|
||||||
|
import { DataSetProps } from '../types';
|
||||||
|
import { getGraphManagerTableHeaderTitle } from '../utils';
|
||||||
|
import { getCheckBox } from './GetCheckBox';
|
||||||
|
import { getLabel } from './GetLabel';
|
||||||
|
|
||||||
|
export const getGraphManagerTableColumns = ({
|
||||||
|
data,
|
||||||
|
checkBoxOnChangeHandler,
|
||||||
|
graphVisibilityState,
|
||||||
|
labelClickedHandler,
|
||||||
|
yAxisUnit,
|
||||||
|
}: GetGraphManagerTableColumnsProps): ColumnType<DataSetProps>[] => [
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
width: 50,
|
||||||
|
dataIndex: ColumnsKeyAndDataIndex.Index,
|
||||||
|
key: ColumnsKeyAndDataIndex.Index,
|
||||||
|
...getCheckBox({
|
||||||
|
checkBoxOnChangeHandler,
|
||||||
|
graphVisibilityState,
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: ColumnsTitle[ColumnsKeyAndDataIndex.Label],
|
||||||
|
width: 300,
|
||||||
|
dataIndex: ColumnsKeyAndDataIndex.Label,
|
||||||
|
key: ColumnsKeyAndDataIndex.Label,
|
||||||
|
...getLabel(labelClickedHandler),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: getGraphManagerTableHeaderTitle(
|
||||||
|
ColumnsTitle[ColumnsKeyAndDataIndex.Avg],
|
||||||
|
yAxisUnit,
|
||||||
|
),
|
||||||
|
width: 90,
|
||||||
|
dataIndex: ColumnsKeyAndDataIndex.Avg,
|
||||||
|
key: ColumnsKeyAndDataIndex.Avg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: getGraphManagerTableHeaderTitle(
|
||||||
|
ColumnsTitle[ColumnsKeyAndDataIndex.Sum],
|
||||||
|
yAxisUnit,
|
||||||
|
),
|
||||||
|
width: 90,
|
||||||
|
dataIndex: ColumnsKeyAndDataIndex.Sum,
|
||||||
|
key: ColumnsKeyAndDataIndex.Sum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: getGraphManagerTableHeaderTitle(
|
||||||
|
ColumnsTitle[ColumnsKeyAndDataIndex.Max],
|
||||||
|
yAxisUnit,
|
||||||
|
),
|
||||||
|
width: 90,
|
||||||
|
dataIndex: ColumnsKeyAndDataIndex.Max,
|
||||||
|
key: ColumnsKeyAndDataIndex.Max,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: getGraphManagerTableHeaderTitle(
|
||||||
|
ColumnsTitle[ColumnsKeyAndDataIndex.Min],
|
||||||
|
yAxisUnit,
|
||||||
|
),
|
||||||
|
width: 90,
|
||||||
|
dataIndex: ColumnsKeyAndDataIndex.Min,
|
||||||
|
key: ColumnsKeyAndDataIndex.Min,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface GetGraphManagerTableColumnsProps {
|
||||||
|
data: ChartData;
|
||||||
|
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||||
|
labelClickedHandler: (labelIndex: number) => void;
|
||||||
|
graphVisibilityState: boolean[];
|
||||||
|
yAxisUnit?: string;
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
import { LabelContainer } from '../styles';
|
||||||
|
import { LabelProps } from '../types';
|
||||||
|
import { getAbbreviatedLabel } from '../utils';
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
labelClickedHandler,
|
||||||
|
labelIndex,
|
||||||
|
label,
|
||||||
|
}: LabelProps): JSX.Element {
|
||||||
|
const onClickHandler = (): void => {
|
||||||
|
labelClickedHandler(labelIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LabelContainer type="button" onClick={onClickHandler}>
|
||||||
|
{getAbbreviatedLabel(label)}
|
||||||
|
</LabelContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Label;
|
@ -0,0 +1,29 @@
|
|||||||
|
import { PanelTypeAndGraphManagerVisibilityProps } from './types';
|
||||||
|
|
||||||
|
export enum ColumnsKeyAndDataIndex {
|
||||||
|
Index = 'index',
|
||||||
|
Legend = 'legend',
|
||||||
|
Label = 'label',
|
||||||
|
Avg = 'avg',
|
||||||
|
Sum = 'sum',
|
||||||
|
Max = 'max',
|
||||||
|
Min = 'min',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ColumnsTitle = {
|
||||||
|
[ColumnsKeyAndDataIndex.Index]: 'Index',
|
||||||
|
[ColumnsKeyAndDataIndex.Label]: 'Label',
|
||||||
|
[ColumnsKeyAndDataIndex.Avg]: 'Avg',
|
||||||
|
[ColumnsKeyAndDataIndex.Sum]: 'Sum',
|
||||||
|
[ColumnsKeyAndDataIndex.Max]: 'Max',
|
||||||
|
[ColumnsKeyAndDataIndex.Min]: 'Min',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityProps = {
|
||||||
|
TIME_SERIES: true,
|
||||||
|
VALUE: false,
|
||||||
|
TABLE: false,
|
||||||
|
LIST: false,
|
||||||
|
TRACE: false,
|
||||||
|
EMPTY_WIDGET: false,
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { GraphOnClickHandler } from 'components/Graph';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import TimePreference from 'components/TimePreferenceDropDown';
|
import TimePreference from 'components/TimePreferenceDropDown';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
@ -9,15 +9,20 @@ import {
|
|||||||
} from 'container/NewWidget/RightContainer/timeItems';
|
} from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
|
import { useChartMutable } from 'hooks/useChartMutable';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import { TimeContainer } from './styles';
|
import { toggleGraphsVisibilityInChart } from '../utils';
|
||||||
|
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||||
|
import GraphManager from './GraphManager';
|
||||||
|
import { GraphContainer, TimeContainer } from './styles';
|
||||||
|
import { FullViewProps } from './types';
|
||||||
|
import { getIsGraphLegendToggleAvailable } from './utils';
|
||||||
|
|
||||||
function FullView({
|
function FullView({
|
||||||
widget,
|
widget,
|
||||||
@ -27,6 +32,8 @@ function FullView({
|
|||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
isDependedDataLoaded = false,
|
isDependedDataLoaded = false,
|
||||||
|
graphsVisibilityStates,
|
||||||
|
onToggleModelHandler,
|
||||||
}: FullViewProps): JSX.Element {
|
}: FullViewProps): JSX.Element {
|
||||||
const { selectedTime: globalSelectedTime } = useSelector<
|
const { selectedTime: globalSelectedTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@ -39,6 +46,22 @@ function FullView({
|
|||||||
[widget],
|
[widget],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canModifyChart = useChartMutable({
|
||||||
|
panelType: widget.panelTypes,
|
||||||
|
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lineChartRef = useRef<ToggleGraphProps>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (graphsVisibilityStates && canModifyChart && lineChartRef.current) {
|
||||||
|
toggleGraphsVisibilityInChart({
|
||||||
|
graphsVisibilityStates,
|
||||||
|
lineChartRef,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [graphsVisibilityStates, canModifyChart]);
|
||||||
|
|
||||||
const [selectedTime, setSelectedTime] = useState<timePreferance>({
|
const [selectedTime, setSelectedTime] = useState<timePreferance>({
|
||||||
name: getSelectedTime()?.name || '',
|
name: getSelectedTime()?.name || '',
|
||||||
enum: widget?.timePreferance || 'GLOBAL_TIME',
|
enum: widget?.timePreferance || 'GLOBAL_TIME',
|
||||||
@ -78,7 +101,11 @@ function FullView({
|
|||||||
[response],
|
[response],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.status === 'idle' || response.status === 'loading') {
|
const isGraphLegendToggleAvailable = getIsGraphLegendToggleAvailable(
|
||||||
|
widget.panelTypes,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.isFetching) {
|
||||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,33 +128,35 @@ function FullView({
|
|||||||
</TimeContainer>
|
</TimeContainer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<GridPanelSwitch
|
<GraphContainer isGraphLegendToggleAvailable={isGraphLegendToggleAvailable}>
|
||||||
panelType={widget.panelTypes}
|
<GridPanelSwitch
|
||||||
data={chartDataSet}
|
panelType={widget.panelTypes}
|
||||||
isStacked={widget.isStacked}
|
data={chartDataSet}
|
||||||
opacity={widget.opacity}
|
isStacked={widget.isStacked}
|
||||||
title={widget.title}
|
opacity={widget.opacity}
|
||||||
onClickHandler={onClickHandler}
|
title={widget.title}
|
||||||
name={name}
|
onClickHandler={onClickHandler}
|
||||||
yAxisUnit={yAxisUnit}
|
name={name}
|
||||||
onDragSelect={onDragSelect}
|
yAxisUnit={yAxisUnit}
|
||||||
panelData={response.data?.payload.data.newResult.data.result || []}
|
onDragSelect={onDragSelect}
|
||||||
query={widget.query}
|
panelData={response.data?.payload.data.newResult.data.result || []}
|
||||||
/>
|
query={widget.query}
|
||||||
|
ref={lineChartRef}
|
||||||
|
/>
|
||||||
|
</GraphContainer>
|
||||||
|
|
||||||
|
{canModifyChart && (
|
||||||
|
<GraphManager
|
||||||
|
data={chartDataSet}
|
||||||
|
name={name}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
onToggleModelHandler={onToggleModelHandler}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FullViewProps {
|
|
||||||
widget: Widgets;
|
|
||||||
fullViewOptions?: boolean;
|
|
||||||
onClickHandler?: GraphOnClickHandler;
|
|
||||||
name: string;
|
|
||||||
yAxisUnit?: string;
|
|
||||||
onDragSelect?: (start: number, end: number) => void;
|
|
||||||
isDependedDataLoaded?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
FullView.defaultProps = {
|
FullView.defaultProps = {
|
||||||
fullViewOptions: undefined,
|
fullViewOptions: undefined,
|
||||||
onClickHandler: undefined,
|
onClickHandler: undefined,
|
||||||
@ -136,4 +165,6 @@ FullView.defaultProps = {
|
|||||||
isDependedDataLoaded: undefined,
|
isDependedDataLoaded: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
FullView.displayName = 'FullView';
|
||||||
|
|
||||||
export default FullView;
|
export default FullView;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||||
|
|
||||||
|
import { GraphContainerProps } from './types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
$panelType: PANEL_TYPES;
|
$panelType: PANEL_TYPES;
|
||||||
}
|
}
|
||||||
@ -22,3 +25,36 @@ export const TimeContainer = styled.div<Props>`
|
|||||||
`
|
`
|
||||||
: css``}
|
: css``}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const GraphContainer = styled.div<GraphContainerProps>`
|
||||||
|
height: ${({ isGraphLegendToggleAvailable }): string =>
|
||||||
|
isGraphLegendToggleAvailable ? '50%' : '100%'};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FilterTableAndSaveContainer = styled.div`
|
||||||
|
margin-top: 1.875rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FilterTableContainer = styled.div`
|
||||||
|
flex-basis: 80%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SaveContainer = styled.div`
|
||||||
|
flex-basis: 20%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SaveCancelButtonContainer = styled.span`
|
||||||
|
margin: 0 0.313rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LabelContainer = styled.button`
|
||||||
|
max-width: 18.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: ${themeColors.white};
|
||||||
|
`;
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
|
import { ChartData, ChartDataset } from 'chart.js';
|
||||||
|
import { GraphOnClickHandler } from 'components/Graph/types';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
export interface DataSetProps {
|
||||||
|
index: number;
|
||||||
|
data: number | null;
|
||||||
|
label: string;
|
||||||
|
borderWidth: number;
|
||||||
|
spanGaps: boolean;
|
||||||
|
animations: boolean;
|
||||||
|
borderColor: string;
|
||||||
|
showLine: boolean;
|
||||||
|
pointRadius: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegendEntryProps {
|
||||||
|
label: string;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtendedChartDataset = ChartDataset & {
|
||||||
|
show: boolean;
|
||||||
|
sum: number;
|
||||||
|
avg: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PanelTypeAndGraphManagerVisibilityProps = Record<
|
||||||
|
keyof typeof PANEL_TYPES,
|
||||||
|
boolean
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface LabelProps {
|
||||||
|
labelClickedHandler: (labelIndex: number) => void;
|
||||||
|
labelIndex: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphManagerProps {
|
||||||
|
data: ChartData;
|
||||||
|
name: string;
|
||||||
|
yAxisUnit?: string;
|
||||||
|
onToggleModelHandler?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckBoxProps {
|
||||||
|
data: ChartData;
|
||||||
|
index: number;
|
||||||
|
graphVisibilityState: boolean[];
|
||||||
|
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullViewProps {
|
||||||
|
widget: Widgets;
|
||||||
|
fullViewOptions?: boolean;
|
||||||
|
onClickHandler?: GraphOnClickHandler;
|
||||||
|
name: string;
|
||||||
|
yAxisUnit?: string;
|
||||||
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
|
isDependedDataLoaded?: boolean;
|
||||||
|
graphsVisibilityStates?: boolean[];
|
||||||
|
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaveLegendEntriesToLocalStoreProps {
|
||||||
|
data: ChartData;
|
||||||
|
graphVisibilityState: boolean[];
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphContainerProps {
|
||||||
|
isGraphLegendToggleAvailable: boolean;
|
||||||
|
}
|
123
frontend/src/container/GridGraphLayout/Graph/FullView/utils.ts
Normal file
123
frontend/src/container/GridGraphLayout/Graph/FullView/utils.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { ChartData, ChartDataset } from 'chart.js';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExtendedChartDataset,
|
||||||
|
LegendEntryProps,
|
||||||
|
SaveLegendEntriesToLocalStoreProps,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
function convertToTwoDecimalsOrZero(value: number): number {
|
||||||
|
if (
|
||||||
|
typeof value === 'number' &&
|
||||||
|
!Number.isNaN(value) &&
|
||||||
|
value !== Infinity &&
|
||||||
|
value !== -Infinity
|
||||||
|
) {
|
||||||
|
const result = value ? value.toFixed(20).match(/^-?\d*\.?0*\d{0,2}/) : null;
|
||||||
|
return result ? parseFloat(result[0]) : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDefaultTableDataSet = (
|
||||||
|
data: ChartData,
|
||||||
|
): ExtendedChartDataset[] =>
|
||||||
|
data.datasets.map(
|
||||||
|
(item: ChartDataset): ExtendedChartDataset => {
|
||||||
|
if (item.data.length === 0) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
show: true,
|
||||||
|
sum: 0,
|
||||||
|
avg: 0,
|
||||||
|
max: 0,
|
||||||
|
min: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
show: true,
|
||||||
|
sum: convertToTwoDecimalsOrZero(
|
||||||
|
(item.data as number[]).reduce((a, b) => a + b, 0),
|
||||||
|
),
|
||||||
|
avg: convertToTwoDecimalsOrZero(
|
||||||
|
(item.data as number[]).reduce((a, b) => a + b, 0) / item.data.length,
|
||||||
|
),
|
||||||
|
max: convertToTwoDecimalsOrZero(Math.max(...(item.data as number[]))),
|
||||||
|
min: convertToTwoDecimalsOrZero(Math.min(...(item.data as number[]))),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getAbbreviatedLabel = (label: string): string => {
|
||||||
|
let newLabel = label;
|
||||||
|
if (label.length > 30) {
|
||||||
|
newLabel = `${label.substring(0, 30)}...`;
|
||||||
|
}
|
||||||
|
return newLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showAllDataSet = (data: ChartData): LegendEntryProps[] =>
|
||||||
|
data.datasets.map(
|
||||||
|
(item): LegendEntryProps => ({
|
||||||
|
label: item.label || '',
|
||||||
|
show: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const saveLegendEntriesToLocalStorage = ({
|
||||||
|
data,
|
||||||
|
graphVisibilityState,
|
||||||
|
name,
|
||||||
|
}: SaveLegendEntriesToLocalStoreProps): void => {
|
||||||
|
const newLegendEntry = {
|
||||||
|
name,
|
||||||
|
dataIndex: data.datasets.map(
|
||||||
|
(item, index): LegendEntryProps => ({
|
||||||
|
label: item.label || '',
|
||||||
|
show: graphVisibilityState[index],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let existingEntries: { name: string; dataIndex: LegendEntryProps[] }[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
existingEntries = JSON.parse(
|
||||||
|
localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) || '[]',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing LEGEND_GRAPH from local storage', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryIndex = existingEntries.findIndex((entry) => entry.name === name);
|
||||||
|
|
||||||
|
if (entryIndex >= 0) {
|
||||||
|
existingEntries[entryIndex] = newLegendEntry;
|
||||||
|
} else {
|
||||||
|
existingEntries = [...existingEntries, newLegendEntry];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
|
||||||
|
JSON.stringify(existingEntries),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting LEGEND_GRAPH to local storage', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIsGraphLegendToggleAvailable = (
|
||||||
|
panelType: PANEL_TYPES,
|
||||||
|
): boolean => panelType === PANEL_TYPES.TIME_SERIES;
|
||||||
|
|
||||||
|
export const getGraphManagerTableHeaderTitle = (
|
||||||
|
title: string,
|
||||||
|
yAxisUnit?: string,
|
||||||
|
): string => {
|
||||||
|
const yAxisUnitText = yAxisUnit ? `(in ${yAxisUnit})` : '';
|
||||||
|
return `${title} ${yAxisUnitText}`;
|
||||||
|
};
|
62
frontend/src/container/GridGraphLayout/Graph/Graph.test.tsx
Normal file
62
frontend/src/container/GridGraphLayout/Graph/Graph.test.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { mockTestData } from './__mock__/mockChartData';
|
||||||
|
import { mocklegendEntryResult } from './__mock__/mockLegendEntryData';
|
||||||
|
import { showAllDataSet } from './FullView/utils';
|
||||||
|
import { getGraphVisibilityStateOnDataChange } from './utils';
|
||||||
|
|
||||||
|
describe('getGraphVisibilityStateOnDataChange', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const localStorageMock = {
|
||||||
|
getItem: jest.fn(),
|
||||||
|
};
|
||||||
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct visibility state and legend entry', () => {
|
||||||
|
// Mock the localStorage behavior
|
||||||
|
const mockLocalStorageData = [
|
||||||
|
{
|
||||||
|
name: 'exampleexpanded',
|
||||||
|
dataIndex: [
|
||||||
|
{ label: 'customer', show: true },
|
||||||
|
{ label: 'demo-app', show: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
jest
|
||||||
|
.spyOn(window.localStorage, 'getItem')
|
||||||
|
.mockReturnValue(JSON.stringify(mockLocalStorageData));
|
||||||
|
|
||||||
|
const result1 = getGraphVisibilityStateOnDataChange({
|
||||||
|
data: mockTestData,
|
||||||
|
isExpandedName: true,
|
||||||
|
name: 'example',
|
||||||
|
});
|
||||||
|
expect(result1.graphVisibilityStates).toEqual([true, false]);
|
||||||
|
expect(result1.legendEntry).toEqual(mocklegendEntryResult);
|
||||||
|
|
||||||
|
const result2 = getGraphVisibilityStateOnDataChange({
|
||||||
|
data: mockTestData,
|
||||||
|
isExpandedName: false,
|
||||||
|
name: 'example',
|
||||||
|
});
|
||||||
|
expect(result2.graphVisibilityStates).toEqual(
|
||||||
|
Array(mockTestData.datasets.length).fill(true),
|
||||||
|
);
|
||||||
|
expect(result2.legendEntry).toEqual(showAllDataSet(mockTestData));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default values if localStorage data is not available', () => {
|
||||||
|
// Mock the localStorage behavior to return null
|
||||||
|
jest.spyOn(window.localStorage, 'getItem').mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = getGraphVisibilityStateOnDataChange({
|
||||||
|
data: mockTestData,
|
||||||
|
isExpandedName: true,
|
||||||
|
name: 'example',
|
||||||
|
});
|
||||||
|
expect(result.graphVisibilityStates).toEqual(
|
||||||
|
Array(mockTestData.datasets.length).fill(true),
|
||||||
|
);
|
||||||
|
expect(result.legendEntry).toEqual(showAllDataSet(mockTestData));
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,336 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
|
import { Events } from 'constants/events';
|
||||||
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
|
import { useChartMutable } from 'hooks/useChartMutable';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { isEmpty, isEqual } from 'lodash-es';
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
memo,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { connect, useSelector } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { DeleteWidget } from 'store/actions/dashboard/deleteWidget';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
|
import { eventEmitter } from 'utils/getEventEmitter';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { UpdateDashboard } from '../utils';
|
||||||
|
import WidgetHeader from '../WidgetHeader';
|
||||||
|
import FullView from './FullView';
|
||||||
|
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './FullView/contants';
|
||||||
|
import { FullViewContainer, Modal } from './styles';
|
||||||
|
import { DispatchProps, WidgetGraphComponentProps } from './types';
|
||||||
|
import {
|
||||||
|
getGraphVisibilityStateOnDataChange,
|
||||||
|
toggleGraphsVisibilityInChart,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function WidgetGraphComponent({
|
||||||
|
enableModel,
|
||||||
|
enableWidgetHeader,
|
||||||
|
data,
|
||||||
|
widget,
|
||||||
|
queryResponse,
|
||||||
|
errorMessage,
|
||||||
|
name,
|
||||||
|
yAxisUnit,
|
||||||
|
layout = [],
|
||||||
|
deleteWidget,
|
||||||
|
setLayout,
|
||||||
|
onDragSelect,
|
||||||
|
onClickHandler,
|
||||||
|
allowClone = true,
|
||||||
|
allowDelete = true,
|
||||||
|
allowEdit = true,
|
||||||
|
}: WidgetGraphComponentProps): JSX.Element {
|
||||||
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
|
const [modal, setModal] = useState<boolean>(false);
|
||||||
|
const [hovered, setHovered] = useState(false);
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
const { t } = useTranslation(['common']);
|
||||||
|
|
||||||
|
const { graphVisibilityStates: localstoredVisibilityStates } = useMemo(
|
||||||
|
() =>
|
||||||
|
getGraphVisibilityStateOnDataChange({
|
||||||
|
data,
|
||||||
|
isExpandedName: true,
|
||||||
|
name,
|
||||||
|
}),
|
||||||
|
[data, name],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [graphsVisibilityStates, setGraphsVisilityStates] = useState<boolean[]>(
|
||||||
|
localstoredVisibilityStates,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||||
|
(state) => state.dashboards,
|
||||||
|
);
|
||||||
|
const [selectedDashboard] = dashboards;
|
||||||
|
|
||||||
|
const canModifyChart = useChartMutable({
|
||||||
|
panelType: widget.panelTypes,
|
||||||
|
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lineChartRef = useRef<ToggleGraphProps>();
|
||||||
|
|
||||||
|
// Updating the visibility state of the graph on data change according to global time range
|
||||||
|
useEffect(() => {
|
||||||
|
if (canModifyChart) {
|
||||||
|
const newGraphVisibilityState = getGraphVisibilityStateOnDataChange({
|
||||||
|
data,
|
||||||
|
isExpandedName: true,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
setGraphsVisilityStates(newGraphVisibilityState.graphVisibilityStates);
|
||||||
|
}
|
||||||
|
}, [canModifyChart, data, name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const eventListener = eventEmitter.on(
|
||||||
|
Events.UPDATE_GRAPH_VISIBILITY_STATE,
|
||||||
|
(data) => {
|
||||||
|
if (data.name === `${name}expanded` && canModifyChart) {
|
||||||
|
setGraphsVisilityStates([...data.graphVisibilityStates]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return (): void => {
|
||||||
|
eventListener.off(Events.UPDATE_GRAPH_VISIBILITY_STATE);
|
||||||
|
};
|
||||||
|
}, [canModifyChart, name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canModifyChart && lineChartRef.current) {
|
||||||
|
toggleGraphsVisibilityInChart({
|
||||||
|
graphsVisibilityStates,
|
||||||
|
lineChartRef,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [graphsVisibilityStates, canModifyChart]);
|
||||||
|
|
||||||
|
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||||
|
(state) => state.app,
|
||||||
|
);
|
||||||
|
const onToggleModal = useCallback(
|
||||||
|
(func: Dispatch<SetStateAction<boolean>>) => {
|
||||||
|
func((value) => !value);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDeleteHandler = useCallback(() => {
|
||||||
|
const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget);
|
||||||
|
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
|
||||||
|
|
||||||
|
featureResponse
|
||||||
|
.refetch()
|
||||||
|
.then(() => {
|
||||||
|
deleteWidget({ widgetId, setLayout });
|
||||||
|
onToggleModal(setDeleteModal);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
notifications.error({
|
||||||
|
message: t('common:something_went_wrong'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
widget,
|
||||||
|
layout,
|
||||||
|
featureResponse,
|
||||||
|
deleteWidget,
|
||||||
|
setLayout,
|
||||||
|
onToggleModal,
|
||||||
|
notifications,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onCloneHandler = async (): Promise<void> => {
|
||||||
|
const uuid = v4();
|
||||||
|
|
||||||
|
const layout = [
|
||||||
|
{
|
||||||
|
i: uuid,
|
||||||
|
w: 6,
|
||||||
|
x: 0,
|
||||||
|
h: 2,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
...(selectedDashboard.data.layout || []),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (widget) {
|
||||||
|
await UpdateDashboard(
|
||||||
|
{
|
||||||
|
data: selectedDashboard.data,
|
||||||
|
generateWidgetId: uuid,
|
||||||
|
graphType: widget?.panelTypes,
|
||||||
|
selectedDashboard,
|
||||||
|
layout,
|
||||||
|
widgetData: widget,
|
||||||
|
isRedirected: false,
|
||||||
|
},
|
||||||
|
notifications,
|
||||||
|
).then(() => {
|
||||||
|
notifications.success({
|
||||||
|
message: 'Panel cloned successfully, redirecting to new copy.',
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
history.push(
|
||||||
|
`${history.location.pathname}/new?graphType=${widget?.panelTypes}&widgetId=${uuid}`,
|
||||||
|
);
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnView = (): void => {
|
||||||
|
onToggleModal(setModal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnDelete = (): void => {
|
||||||
|
onToggleModal(setDeleteModal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteModelHandler = (): void => {
|
||||||
|
onToggleModal(setDeleteModal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleModelHandler = (): void => {
|
||||||
|
onToggleModal(setModal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModals = (): JSX.Element => (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
destroyOnClose
|
||||||
|
onCancel={onDeleteModelHandler}
|
||||||
|
open={deleteModal}
|
||||||
|
title="Delete"
|
||||||
|
height="10vh"
|
||||||
|
onOk={onDeleteHandler}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Typography>Are you sure you want to delete this widget</Typography>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="View"
|
||||||
|
footer={[]}
|
||||||
|
centered
|
||||||
|
open={modal}
|
||||||
|
onCancel={onToggleModelHandler}
|
||||||
|
width="85%"
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<FullViewContainer>
|
||||||
|
<FullView
|
||||||
|
name={`${name}expanded`}
|
||||||
|
widget={widget}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
graphsVisibilityStates={graphsVisibilityStates}
|
||||||
|
onToggleModelHandler={onToggleModelHandler}
|
||||||
|
/>
|
||||||
|
</FullViewContainer>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
onMouseOver={(): void => {
|
||||||
|
setHovered(true);
|
||||||
|
}}
|
||||||
|
onFocus={(): void => {
|
||||||
|
setHovered(true);
|
||||||
|
}}
|
||||||
|
onMouseOut={(): void => {
|
||||||
|
setHovered(false);
|
||||||
|
}}
|
||||||
|
onBlur={(): void => {
|
||||||
|
setHovered(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{enableModel && getModals()}
|
||||||
|
{!isEmpty(widget) && data && (
|
||||||
|
<>
|
||||||
|
{enableWidgetHeader && (
|
||||||
|
<div className="drag-handle">
|
||||||
|
<WidgetHeader
|
||||||
|
parentHover={hovered}
|
||||||
|
title={widget?.title}
|
||||||
|
widget={widget}
|
||||||
|
onView={handleOnView}
|
||||||
|
onDelete={handleOnDelete}
|
||||||
|
onClone={onCloneHandler}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
allowClone={allowClone}
|
||||||
|
allowDelete={allowDelete}
|
||||||
|
allowEdit={allowEdit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<GridPanelSwitch
|
||||||
|
panelType={widget.panelTypes}
|
||||||
|
data={data}
|
||||||
|
isStacked={widget.isStacked}
|
||||||
|
opacity={widget.opacity}
|
||||||
|
title={' '}
|
||||||
|
name={name}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
onClickHandler={onClickHandler}
|
||||||
|
onDragSelect={onDragSelect}
|
||||||
|
panelData={[]}
|
||||||
|
query={widget.query}
|
||||||
|
ref={lineChartRef}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetGraphComponent.defaultProps = {
|
||||||
|
yAxisUnit: undefined,
|
||||||
|
layout: undefined,
|
||||||
|
setLayout: undefined,
|
||||||
|
onDragSelect: undefined,
|
||||||
|
onClickHandler: undefined,
|
||||||
|
allowDelete: true,
|
||||||
|
allowClone: true,
|
||||||
|
allowEdit: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
null,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(
|
||||||
|
memo(
|
||||||
|
WidgetGraphComponent,
|
||||||
|
(prevProps, nextProps) =>
|
||||||
|
isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name,
|
||||||
|
),
|
||||||
|
);
|
@ -0,0 +1,15 @@
|
|||||||
|
import { ChartData } from 'chart.js';
|
||||||
|
|
||||||
|
export const mockTestData: ChartData = {
|
||||||
|
labels: ['test1', 'test2'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'customer',
|
||||||
|
data: [481.60377358490564, 730.0000000000002],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'demo-app',
|
||||||
|
data: [4471.4285714285725],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,12 @@
|
|||||||
|
import { LegendEntryProps } from '../FullView/types';
|
||||||
|
|
||||||
|
export const mocklegendEntryResult: LegendEntryProps[] = [
|
||||||
|
{
|
||||||
|
label: 'customer',
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'demo-app',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
];
|
@ -1,56 +1,25 @@
|
|||||||
import { Typography } from 'antd';
|
|
||||||
import { ChartData } from 'chart.js';
|
import { ChartData } from 'chart.js';
|
||||||
import { GraphOnClickHandler } from 'components/Graph';
|
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { UpdateDashboard } from 'container/GridGraphLayout/utils';
|
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import usePreviousValue from 'hooks/usePreviousValue';
|
import usePreviousValue from 'hooks/usePreviousValue';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import history from 'lib/history';
|
|
||||||
import isEmpty from 'lodash-es/isEmpty';
|
import isEmpty from 'lodash-es/isEmpty';
|
||||||
import {
|
import { memo, useMemo, useState } from 'react';
|
||||||
Dispatch,
|
|
||||||
memo,
|
|
||||||
SetStateAction,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { Layout } from 'react-grid-layout';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
|
||||||
import {
|
|
||||||
DeleteWidget,
|
|
||||||
DeleteWidgetProps,
|
|
||||||
} from 'store/actions/dashboard/deleteWidget';
|
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import {
|
import { getSelectedDashboardVariable } from 'utils/dashboard/selectedDashboard';
|
||||||
getSelectedDashboard,
|
|
||||||
getSelectedDashboardVariable,
|
|
||||||
} from 'utils/dashboard/selectedDashboard';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
import { LayoutProps } from '..';
|
|
||||||
import EmptyWidget from '../EmptyWidget';
|
import EmptyWidget from '../EmptyWidget';
|
||||||
import WidgetHeader from '../WidgetHeader';
|
import { GridCardGraphProps } from './types';
|
||||||
import FullView from './FullView';
|
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||||
import { FullViewContainer, Modal } from './styles';
|
|
||||||
|
|
||||||
function GridCardGraph({
|
function GridCardGraph({
|
||||||
widget,
|
widget,
|
||||||
deleteWidget,
|
|
||||||
name,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
layout = [],
|
layout = [],
|
||||||
@ -72,27 +41,16 @@ function GridCardGraph({
|
|||||||
initialInView: false,
|
initialInView: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const { t } = useTranslation(['common']);
|
|
||||||
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||||
const [hovered, setHovered] = useState(false);
|
|
||||||
const [modal, setModal] = useState(false);
|
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
|
||||||
|
|
||||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
|
||||||
(state) => state.app,
|
|
||||||
);
|
|
||||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||||
(state) => state.dashboards,
|
(state) => state.dashboards,
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedDashboard = getSelectedDashboard(dashboards);
|
|
||||||
const variables = getSelectedDashboardVariable(dashboards);
|
const variables = getSelectedDashboardVariable(dashboards);
|
||||||
|
|
||||||
const updatedQuery = useStepInterval(widget?.query);
|
const updatedQuery = useStepInterval(widget?.query);
|
||||||
@ -142,153 +100,31 @@ function GridCardGraph({
|
|||||||
|
|
||||||
const prevChartDataSetRef = usePreviousValue<ChartData>(chartData);
|
const prevChartDataSetRef = usePreviousValue<ChartData>(chartData);
|
||||||
|
|
||||||
const onToggleModal = useCallback(
|
|
||||||
(func: Dispatch<SetStateAction<boolean>>) => {
|
|
||||||
func((value) => !value);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDeleteHandler = useCallback(() => {
|
|
||||||
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
|
|
||||||
|
|
||||||
featureResponse
|
|
||||||
.refetch()
|
|
||||||
.then(() => {
|
|
||||||
deleteWidget({ widgetId, setLayout });
|
|
||||||
onToggleModal(setDeleteModal);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
notifications.error({
|
|
||||||
message: t('common:something_went_wrong'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
isEmptyWidget,
|
|
||||||
widget?.id,
|
|
||||||
layout,
|
|
||||||
featureResponse,
|
|
||||||
deleteWidget,
|
|
||||||
setLayout,
|
|
||||||
onToggleModal,
|
|
||||||
notifications,
|
|
||||||
t,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onCloneHandler = async (): Promise<void> => {
|
|
||||||
const uuid = v4();
|
|
||||||
|
|
||||||
const layout = [
|
|
||||||
{
|
|
||||||
i: uuid,
|
|
||||||
w: 6,
|
|
||||||
x: 0,
|
|
||||||
h: 2,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
...(selectedDashboard?.data.layout || []),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (widget && selectedDashboard) {
|
|
||||||
await UpdateDashboard(
|
|
||||||
{
|
|
||||||
data: selectedDashboard.data,
|
|
||||||
generateWidgetId: uuid,
|
|
||||||
graphType: widget?.panelTypes,
|
|
||||||
selectedDashboard,
|
|
||||||
layout,
|
|
||||||
widgetData: widget,
|
|
||||||
isRedirected: false,
|
|
||||||
},
|
|
||||||
notifications,
|
|
||||||
).then(() => {
|
|
||||||
notifications.success({
|
|
||||||
message: 'Panel cloned successfully, redirecting to new copy.',
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
history.push(
|
|
||||||
`${history.location.pathname}/new?graphType=${widget?.panelTypes}&widgetId=${uuid}`,
|
|
||||||
);
|
|
||||||
}, 1500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getModals = (): JSX.Element => (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
destroyOnClose
|
|
||||||
onCancel={(): void => onToggleModal(setDeleteModal)}
|
|
||||||
open={deleteModal}
|
|
||||||
title="Delete"
|
|
||||||
height="10vh"
|
|
||||||
onOk={onDeleteHandler}
|
|
||||||
centered
|
|
||||||
>
|
|
||||||
<Typography>Are you sure you want to delete this widget</Typography>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="View"
|
|
||||||
footer={[]}
|
|
||||||
centered
|
|
||||||
open={modal}
|
|
||||||
onCancel={(): void => onToggleModal(setModal)}
|
|
||||||
width="85%"
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
<FullViewContainer>
|
|
||||||
<FullView name={`${name}expanded`} widget={widget} yAxisUnit={yAxisUnit} />
|
|
||||||
</FullViewContainer>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOnView = (): void => {
|
|
||||||
onToggleModal(setModal);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnDelete = (): void => {
|
|
||||||
onToggleModal(setDeleteModal);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
||||||
|
|
||||||
|
if (queryResponse.isRefetching) {
|
||||||
|
return <Spinner height="20vh" tip="Loading..." />;
|
||||||
|
}
|
||||||
|
|
||||||
if (queryResponse.isError && !isEmptyLayout) {
|
if (queryResponse.isError && !isEmptyLayout) {
|
||||||
return (
|
return (
|
||||||
<span ref={graphRef}>
|
<span ref={graphRef}>
|
||||||
{getModals()}
|
|
||||||
{!isEmpty(widget) && prevChartDataSetRef && (
|
{!isEmpty(widget) && prevChartDataSetRef && (
|
||||||
<>
|
<WidgetGraphComponent
|
||||||
<div className="drag-handle">
|
enableModel
|
||||||
<WidgetHeader
|
enableWidgetHeader
|
||||||
parentHover={hovered}
|
widget={widget}
|
||||||
title={widget?.title}
|
queryResponse={queryResponse}
|
||||||
widget={widget}
|
errorMessage={errorMessage}
|
||||||
onView={handleOnView}
|
data={prevChartDataSetRef}
|
||||||
onDelete={handleOnDelete}
|
name={name}
|
||||||
onClone={onCloneHandler}
|
yAxisUnit={yAxisUnit}
|
||||||
queryResponse={queryResponse}
|
layout={layout}
|
||||||
errorMessage={errorMessage}
|
setLayout={setLayout}
|
||||||
allowClone={allowClone}
|
allowClone={allowClone}
|
||||||
allowDelete={allowDelete}
|
allowDelete={allowDelete}
|
||||||
allowEdit={allowEdit}
|
allowEdit={allowEdit}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<GridPanelSwitch
|
|
||||||
panelType={widget?.panelTypes}
|
|
||||||
data={prevChartDataSetRef}
|
|
||||||
isStacked={widget?.isStacked}
|
|
||||||
opacity={widget?.opacity}
|
|
||||||
title={' '}
|
|
||||||
name={name}
|
|
||||||
yAxisUnit={yAxisUnit}
|
|
||||||
onClickHandler={onClickHandler}
|
|
||||||
panelData={[]}
|
|
||||||
query={widget.query}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -298,35 +134,22 @@ function GridCardGraph({
|
|||||||
return (
|
return (
|
||||||
<span ref={graphRef}>
|
<span ref={graphRef}>
|
||||||
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
|
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
|
||||||
<>
|
<WidgetGraphComponent
|
||||||
<div className="drag-handle">
|
enableModel={false}
|
||||||
<WidgetHeader
|
enableWidgetHeader
|
||||||
parentHover={hovered}
|
widget={widget}
|
||||||
title={widget?.title}
|
queryResponse={queryResponse}
|
||||||
widget={widget}
|
errorMessage={errorMessage}
|
||||||
onView={handleOnView}
|
data={prevChartDataSetRef}
|
||||||
onDelete={handleOnDelete}
|
name={name}
|
||||||
onClone={onCloneHandler}
|
yAxisUnit={yAxisUnit}
|
||||||
queryResponse={queryResponse}
|
layout={layout}
|
||||||
errorMessage={errorMessage}
|
setLayout={setLayout}
|
||||||
allowClone={allowClone}
|
allowClone={allowClone}
|
||||||
allowDelete={allowDelete}
|
allowDelete={allowDelete}
|
||||||
allowEdit={allowEdit}
|
allowEdit={allowEdit}
|
||||||
/>
|
onClickHandler={onClickHandler}
|
||||||
</div>
|
/>
|
||||||
<GridPanelSwitch
|
|
||||||
panelType={widget.panelTypes}
|
|
||||||
data={prevChartDataSetRef}
|
|
||||||
isStacked={widget.isStacked}
|
|
||||||
opacity={widget.opacity}
|
|
||||||
title={' '}
|
|
||||||
name={name}
|
|
||||||
yAxisUnit={yAxisUnit}
|
|
||||||
onClickHandler={onClickHandler}
|
|
||||||
panelData={[]}
|
|
||||||
query={widget.query}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<Spinner height="20vh" tip="Loading..." />
|
<Spinner height="20vh" tip="Loading..." />
|
||||||
)}
|
)}
|
||||||
@ -335,54 +158,21 @@ function GridCardGraph({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span ref={graphRef}>
|
||||||
ref={graphRef}
|
|
||||||
onMouseOver={(): void => {
|
|
||||||
setHovered(true);
|
|
||||||
}}
|
|
||||||
onFocus={(): void => {
|
|
||||||
setHovered(true);
|
|
||||||
}}
|
|
||||||
onMouseOut={(): void => {
|
|
||||||
setHovered(false);
|
|
||||||
}}
|
|
||||||
onBlur={(): void => {
|
|
||||||
setHovered(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!isEmptyLayout && (
|
|
||||||
<div className="drag-handle">
|
|
||||||
<WidgetHeader
|
|
||||||
parentHover={hovered}
|
|
||||||
title={widget?.title}
|
|
||||||
widget={widget}
|
|
||||||
onView={handleOnView}
|
|
||||||
onDelete={handleOnDelete}
|
|
||||||
onClone={onCloneHandler}
|
|
||||||
queryResponse={queryResponse}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
allowClone={allowClone}
|
|
||||||
allowDelete={allowDelete}
|
|
||||||
allowEdit={allowEdit}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isEmptyLayout && getModals()}
|
|
||||||
|
|
||||||
{!isEmpty(widget) && !!queryResponse.data?.payload && (
|
{!isEmpty(widget) && !!queryResponse.data?.payload && (
|
||||||
<GridPanelSwitch
|
<WidgetGraphComponent
|
||||||
panelType={widget.panelTypes}
|
enableModel={!isEmptyLayout}
|
||||||
|
enableWidgetHeader={!isEmptyLayout}
|
||||||
|
widget={widget}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
errorMessage={errorMessage}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
isStacked={widget.isStacked}
|
|
||||||
opacity={widget.opacity}
|
|
||||||
title={' '} // `empty title to accommodate absolutely positioned widget header
|
|
||||||
name={name}
|
name={name}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
onClickHandler={onClickHandler}
|
allowClone={allowClone}
|
||||||
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
|
allowDelete={allowDelete}
|
||||||
query={widget.query}
|
allowEdit={allowEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -391,28 +181,6 @@ function GridCardGraph({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
deleteWidget: ({
|
|
||||||
widgetId,
|
|
||||||
}: DeleteWidgetProps) => (dispatch: Dispatch<AppActions>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GridCardGraphProps extends DispatchProps {
|
|
||||||
widget: Widgets;
|
|
||||||
name: string;
|
|
||||||
yAxisUnit: string | undefined;
|
|
||||||
// eslint-disable-next-line react/require-default-props
|
|
||||||
layout?: Layout[];
|
|
||||||
// eslint-disable-next-line react/require-default-props
|
|
||||||
setLayout?: Dispatch<SetStateAction<LayoutProps[]>>;
|
|
||||||
onDragSelect?: (start: number, end: number) => void;
|
|
||||||
onClickHandler?: GraphOnClickHandler;
|
|
||||||
allowDelete?: boolean;
|
|
||||||
allowClone?: boolean;
|
|
||||||
allowEdit?: boolean;
|
|
||||||
isQueryEnabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
GridCardGraph.defaultProps = {
|
GridCardGraph.defaultProps = {
|
||||||
onDragSelect: undefined,
|
onDragSelect: undefined,
|
||||||
onClickHandler: undefined,
|
onClickHandler: undefined,
|
||||||
@ -422,10 +190,4 @@ GridCardGraph.defaultProps = {
|
|||||||
isQueryEnabled: true,
|
isQueryEnabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
export default memo(GridCardGraph);
|
||||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
|
||||||
): DispatchProps => ({
|
|
||||||
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(memo(GridCardGraph));
|
|
||||||
|
71
frontend/src/container/GridGraphLayout/Graph/types.ts
Normal file
71
frontend/src/container/GridGraphLayout/Graph/types.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { ChartData } from 'chart.js';
|
||||||
|
import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types';
|
||||||
|
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||||
|
import { Layout } from 'react-grid-layout';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { DeleteWidgetProps } from 'store/actions/dashboard/deleteWidget';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
|
||||||
|
import { LayoutProps } from '..';
|
||||||
|
import { LegendEntryProps } from './FullView/types';
|
||||||
|
|
||||||
|
export interface GraphVisibilityLegendEntryProps {
|
||||||
|
graphVisibilityStates: boolean[];
|
||||||
|
legendEntry: LegendEntryProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DispatchProps {
|
||||||
|
deleteWidget: ({
|
||||||
|
widgetId,
|
||||||
|
}: DeleteWidgetProps) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WidgetGraphComponentProps extends DispatchProps {
|
||||||
|
enableModel: boolean;
|
||||||
|
enableWidgetHeader: boolean;
|
||||||
|
widget: Widgets;
|
||||||
|
queryResponse: UseQueryResult<
|
||||||
|
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||||
|
>;
|
||||||
|
errorMessage: string | undefined;
|
||||||
|
data: ChartData;
|
||||||
|
name: string;
|
||||||
|
yAxisUnit?: string;
|
||||||
|
layout?: Layout[];
|
||||||
|
setLayout?: Dispatch<SetStateAction<LayoutProps[]>>;
|
||||||
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
|
onClickHandler?: GraphOnClickHandler;
|
||||||
|
allowDelete?: boolean;
|
||||||
|
allowClone?: boolean;
|
||||||
|
allowEdit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GridCardGraphProps {
|
||||||
|
widget: Widgets;
|
||||||
|
name: string;
|
||||||
|
yAxisUnit: string | undefined;
|
||||||
|
// eslint-disable-next-line react/require-default-props
|
||||||
|
layout?: Layout[];
|
||||||
|
// eslint-disable-next-line react/require-default-props
|
||||||
|
setLayout?: Dispatch<SetStateAction<LayoutProps[]>>;
|
||||||
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
|
onClickHandler?: GraphOnClickHandler;
|
||||||
|
allowDelete?: boolean;
|
||||||
|
allowClone?: boolean;
|
||||||
|
allowEdit?: boolean;
|
||||||
|
isQueryEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetGraphVisibilityStateOnLegendClickProps {
|
||||||
|
data: ChartData;
|
||||||
|
isExpandedName: boolean;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleGraphsVisibilityInChartProps {
|
||||||
|
graphsVisibilityStates: GraphVisibilityLegendEntryProps['graphVisibilityStates'];
|
||||||
|
lineChartRef: MutableRefObject<ToggleGraphProps | undefined>;
|
||||||
|
}
|
66
frontend/src/container/GridGraphLayout/Graph/utils.ts
Normal file
66
frontend/src/container/GridGraphLayout/Graph/utils.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
|
||||||
|
import { LegendEntryProps } from './FullView/types';
|
||||||
|
import { showAllDataSet } from './FullView/utils';
|
||||||
|
import {
|
||||||
|
GetGraphVisibilityStateOnLegendClickProps,
|
||||||
|
GraphVisibilityLegendEntryProps,
|
||||||
|
ToggleGraphsVisibilityInChartProps,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const getGraphVisibilityStateOnDataChange = ({
|
||||||
|
data,
|
||||||
|
isExpandedName,
|
||||||
|
name,
|
||||||
|
}: GetGraphVisibilityStateOnLegendClickProps): GraphVisibilityLegendEntryProps => {
|
||||||
|
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
|
||||||
|
graphVisibilityStates: Array(data.datasets.length).fill(true),
|
||||||
|
legendEntry: showAllDataSet(data),
|
||||||
|
};
|
||||||
|
if (localStorage.getItem(LOCALSTORAGE.GRAPH_VISIBILITY_STATES) !== null) {
|
||||||
|
const legendGraphFromLocalStore = localStorage.getItem(
|
||||||
|
LOCALSTORAGE.GRAPH_VISIBILITY_STATES,
|
||||||
|
);
|
||||||
|
let legendFromLocalStore: {
|
||||||
|
name: string;
|
||||||
|
dataIndex: LegendEntryProps[];
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
legendFromLocalStore = JSON.parse(legendGraphFromLocalStore || '[]');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Error parsing GRAPH_VISIBILITY_STATES from local storage',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGraphVisibilityStates = Array(data.datasets.length).fill(true);
|
||||||
|
legendFromLocalStore.forEach((item) => {
|
||||||
|
const newName = isExpandedName ? `${name}expanded` : name;
|
||||||
|
if (item.name === newName) {
|
||||||
|
visibilityStateAndLegendEntry.legendEntry = item.dataIndex;
|
||||||
|
data.datasets.forEach((datasets, i) => {
|
||||||
|
const index = item.dataIndex.findIndex(
|
||||||
|
(dataKey) => dataKey.label === datasets.label,
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
newGraphVisibilityStates[i] = item.dataIndex[index].show;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
visibilityStateAndLegendEntry.graphVisibilityStates = newGraphVisibilityStates;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibilityStateAndLegendEntry;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleGraphsVisibilityInChart = ({
|
||||||
|
graphsVisibilityStates,
|
||||||
|
lineChartRef,
|
||||||
|
}: ToggleGraphsVisibilityInChartProps): void => {
|
||||||
|
graphsVisibilityStates?.forEach((showLegendData, index) => {
|
||||||
|
lineChartRef?.current?.toggleGraph(index, showLegendData);
|
||||||
|
});
|
||||||
|
};
|
@ -1,73 +1,85 @@
|
|||||||
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import { PANEL_TYPES_COMPONENT_MAP } from 'constants/panelTypes';
|
import { PANEL_TYPES_COMPONENT_MAP } from 'constants/panelTypes';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
||||||
import { FC, memo, useMemo } from 'react';
|
import { FC, forwardRef, memo, useMemo } from 'react';
|
||||||
|
|
||||||
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
|
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
|
||||||
|
|
||||||
function GridPanelSwitch({
|
const GridPanelSwitch = forwardRef<
|
||||||
panelType,
|
ToggleGraphProps | undefined,
|
||||||
data,
|
GridPanelSwitchProps
|
||||||
title,
|
>(
|
||||||
isStacked,
|
(
|
||||||
onClickHandler,
|
{
|
||||||
name,
|
panelType,
|
||||||
yAxisUnit,
|
data,
|
||||||
staticLine,
|
title,
|
||||||
onDragSelect,
|
isStacked,
|
||||||
panelData,
|
onClickHandler,
|
||||||
query,
|
name,
|
||||||
}: GridPanelSwitchProps): JSX.Element | null {
|
yAxisUnit,
|
||||||
const currentProps: PropsTypePropsMap = useMemo(() => {
|
staticLine,
|
||||||
const result: PropsTypePropsMap = {
|
onDragSelect,
|
||||||
[PANEL_TYPES.TIME_SERIES]: {
|
panelData,
|
||||||
type: 'line',
|
query,
|
||||||
data,
|
},
|
||||||
title,
|
ref,
|
||||||
isStacked,
|
): JSX.Element | null => {
|
||||||
onClickHandler,
|
const currentProps: PropsTypePropsMap = useMemo(() => {
|
||||||
name,
|
const result: PropsTypePropsMap = {
|
||||||
yAxisUnit,
|
[PANEL_TYPES.TIME_SERIES]: {
|
||||||
staticLine,
|
type: 'line',
|
||||||
onDragSelect,
|
data,
|
||||||
},
|
title,
|
||||||
[PANEL_TYPES.VALUE]: {
|
isStacked,
|
||||||
title,
|
onClickHandler,
|
||||||
data,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
},
|
staticLine,
|
||||||
[PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query },
|
onDragSelect,
|
||||||
[PANEL_TYPES.LIST]: null,
|
ref,
|
||||||
[PANEL_TYPES.TRACE]: null,
|
},
|
||||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
[PANEL_TYPES.VALUE]: {
|
||||||
};
|
title,
|
||||||
|
data,
|
||||||
|
yAxisUnit,
|
||||||
|
},
|
||||||
|
[PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query },
|
||||||
|
[PANEL_TYPES.LIST]: null,
|
||||||
|
[PANEL_TYPES.TRACE]: null,
|
||||||
|
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||||
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [
|
}, [
|
||||||
data,
|
data,
|
||||||
isStacked,
|
isStacked,
|
||||||
name,
|
name,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
staticLine,
|
staticLine,
|
||||||
title,
|
title,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
panelData,
|
panelData,
|
||||||
query,
|
query,
|
||||||
]);
|
ref,
|
||||||
|
]);
|
||||||
|
|
||||||
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
|
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
|
||||||
PropsTypePropsMap[typeof panelType]
|
PropsTypePropsMap[typeof panelType]
|
||||||
>;
|
>;
|
||||||
const componentProps = useMemo(() => currentProps[panelType], [
|
const componentProps = useMemo(() => currentProps[panelType], [
|
||||||
panelType,
|
panelType,
|
||||||
currentProps,
|
currentProps,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!Component || !componentProps) return null;
|
if (!Component || !componentProps) return null;
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return <Component {...componentProps} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
GridPanelSwitch.displayName = 'GridPanelSwitch';
|
||||||
return <Component {...componentProps} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(GridPanelSwitch);
|
export default memo(GridPanelSwitch);
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
GraphOnClickHandler,
|
GraphOnClickHandler,
|
||||||
GraphProps,
|
GraphProps,
|
||||||
StaticLineProps,
|
StaticLineProps,
|
||||||
} from 'components/Graph';
|
} from 'components/Graph/types';
|
||||||
import { GridTableComponentProps } from 'container/GridTableComponent/types';
|
import { GridTableComponentProps } from 'container/GridTableComponent/types';
|
||||||
import { GridValueComponentProps } from 'container/GridValueComponent/types';
|
import { GridValueComponentProps } from 'container/GridValueComponent/types';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
22
frontend/src/hooks/useChartMutable.ts
Normal file
22
frontend/src/hooks/useChartMutable.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { PanelTypeAndGraphManagerVisibilityProps } from 'container/GridGraphLayout/Graph/FullView/types';
|
||||||
|
import { PanelTypeKeys } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export const useChartMutable = ({
|
||||||
|
panelType,
|
||||||
|
panelTypeAndGraphManagerVisibility,
|
||||||
|
}: UseChartMutableProps): boolean => {
|
||||||
|
const panelKeys: PanelTypeKeys[] = [].slice.call(Object.keys(PANEL_TYPES));
|
||||||
|
const graphType = panelKeys.find(
|
||||||
|
(key: PanelTypeKeys) => PANEL_TYPES[key] === panelType,
|
||||||
|
);
|
||||||
|
if (!graphType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return panelTypeAndGraphManagerVisibility[graphType];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UseChartMutableProps {
|
||||||
|
panelType: string;
|
||||||
|
panelTypeAndGraphManagerVisibility: PanelTypeAndGraphManagerVisibilityProps;
|
||||||
|
}
|
@ -65,6 +65,7 @@ const getChartData = ({
|
|||||||
return {
|
return {
|
||||||
datasets: alldata.map((e, index) => {
|
datasets: alldata.map((e, index) => {
|
||||||
const datasetBaseConfig = {
|
const datasetBaseConfig = {
|
||||||
|
index,
|
||||||
label: allLabels[index],
|
label: allLabels[index],
|
||||||
borderColor: colors[index % colors.length] || 'red',
|
borderColor: colors[index % colors.length] || 'red',
|
||||||
data: e,
|
data: e,
|
||||||
|
3
frontend/src/utils/getEventEmitter.ts
Normal file
3
frontend/src/utils/getEventEmitter.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import EventEmitter from 'eventemitter3';
|
||||||
|
|
||||||
|
export const eventEmitter = new EventEmitter();
|
@ -5896,6 +5896,11 @@ eventemitter3@4.0.7, eventemitter3@^4.0.0:
|
|||||||
resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz"
|
resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz"
|
||||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||||
|
|
||||||
|
eventemitter3@5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4"
|
||||||
|
integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==
|
||||||
|
|
||||||
events@^3.2.0:
|
events@^3.2.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
|
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user