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",
|
||||
"dotenv": "8.2.0",
|
||||
"event-source-polyfill": "1.0.31",
|
||||
"eventemitter3": "5.0.1",
|
||||
"file-loader": "6.1.1",
|
||||
"fontfaceobserver": "2.3.0",
|
||||
"history": "4.10.1",
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Chart, ChartType, Plugin } from 'chart.js';
|
||||
import { Events } from 'constants/events';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
import { get } from 'lodash-es';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
|
||||
const getOrCreateLegendList = (
|
||||
chart: Chart,
|
||||
@ -74,6 +76,10 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
|
||||
item.datasetIndex,
|
||||
!chart.isDatasetVisible(item.datasetIndex),
|
||||
);
|
||||
eventEmitter.emit(Events.UPDATE_GRAPH_MANAGER_TABLE, {
|
||||
name: id,
|
||||
index: item.datasetIndex,
|
||||
});
|
||||
}
|
||||
chart.update();
|
||||
};
|
||||
|
@ -1,12 +1,8 @@
|
||||
import {
|
||||
ActiveElement,
|
||||
BarController,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
Chart,
|
||||
ChartData,
|
||||
ChartEvent,
|
||||
ChartOptions,
|
||||
ChartType,
|
||||
Decimation,
|
||||
Filler,
|
||||
@ -21,33 +17,28 @@ import {
|
||||
Title,
|
||||
Tooltip,
|
||||
} from 'chart.js';
|
||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
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 { getAxisLabelColor } from './helpers';
|
||||
import { legend } from './Plugin';
|
||||
import {
|
||||
createDragSelectPlugin,
|
||||
createDragSelectPluginOptions,
|
||||
dragSelectPluginId,
|
||||
DragSelectPluginOptions,
|
||||
} from './Plugin/DragSelect';
|
||||
import { createDragSelectPlugin } from './Plugin/DragSelect';
|
||||
import { emptyGraph } from './Plugin/EmptyGraph';
|
||||
import {
|
||||
createIntersectionCursorPlugin,
|
||||
createIntersectionCursorPluginOptions,
|
||||
intersectionCursorPluginId,
|
||||
IntersectionCursorPluginOptions,
|
||||
} from './Plugin/IntersectionCursor';
|
||||
import { createIntersectionCursorPlugin } from './Plugin/IntersectionCursor';
|
||||
import { TooltipPosition as TooltipPositionHandler } from './Plugin/Tooltip';
|
||||
import { LegendsContainer } from './styles';
|
||||
import { CustomChartOptions, GraphProps, ToggleGraphProps } from './types';
|
||||
import { getGraphOptions, toggleGraph } from './utils';
|
||||
import { useXAxisTimeUnit } from './xAxisConfig';
|
||||
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
||||
|
||||
Chart.register(
|
||||
LineElement,
|
||||
@ -70,265 +61,125 @@ Chart.register(
|
||||
|
||||
Tooltip.positioners.custom = TooltipPositionHandler;
|
||||
|
||||
function Graph({
|
||||
animate = true,
|
||||
data,
|
||||
type,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit = 'short',
|
||||
forceReRender,
|
||||
staticLine,
|
||||
containerHeight,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
}: GraphProps): JSX.Element {
|
||||
const nearestDatasetIndex = useRef<null | number>(null);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
||||
(
|
||||
{
|
||||
animate = true,
|
||||
data,
|
||||
type,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit = 'short',
|
||||
forceReRender,
|
||||
staticLine,
|
||||
containerHeight,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
},
|
||||
ref,
|
||||
): JSX.Element => {
|
||||
const nearestDatasetIndex = useRef<null | number>(null);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const currentTheme = isDarkMode ? 'dark' : 'light';
|
||||
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
||||
const currentTheme = isDarkMode ? 'dark' : 'light';
|
||||
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
||||
|
||||
const lineChartRef = useRef<Chart>();
|
||||
const getGridColor = useCallback(() => {
|
||||
if (currentTheme === undefined) {
|
||||
return 'rgba(231,233,237,0.1)';
|
||||
}
|
||||
const lineChartRef = useRef<Chart>();
|
||||
|
||||
if (currentTheme === 'dark') {
|
||||
return 'rgba(231,233,237,0.1)';
|
||||
}
|
||||
|
||||
return 'rgba(231,233,237,0.8)';
|
||||
}, [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,
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
(): ToggleGraphProps => ({
|
||||
toggleGraph(graphIndex: number, isVisible: boolean): void {
|
||||
toggleGraph(graphIndex, isVisible, lineChartRef);
|
||||
},
|
||||
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) {
|
||||
label += ': ';
|
||||
}
|
||||
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);
|
||||
const getGridColor = useCallback(() => {
|
||||
if (currentTheme === undefined) {
|
||||
return 'rgba(231,233,237,0.1)';
|
||||
}
|
||||
|
||||
lineChartRef.current = new Chart(chartRef.current, {
|
||||
type,
|
||||
data,
|
||||
options,
|
||||
plugins: chartPlugins,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
animate,
|
||||
title,
|
||||
getGridColor,
|
||||
xAxisTimeUnit?.unitName,
|
||||
xAxisTimeUnit?.stepSize,
|
||||
isStacked,
|
||||
type,
|
||||
data,
|
||||
name,
|
||||
yAxisUnit,
|
||||
onClickHandler,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
dragSelectColor,
|
||||
currentTheme,
|
||||
]);
|
||||
if (currentTheme === 'dark') {
|
||||
return 'rgba(231,233,237,0.1)';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
buildChart();
|
||||
}, [buildChart, forceReRender]);
|
||||
return 'rgba(231,233,237,0.8)';
|
||||
}, [currentTheme]);
|
||||
|
||||
return (
|
||||
<div style={{ height: containerHeight }}>
|
||||
<canvas ref={chartRef} />
|
||||
<LegendsContainer id={name} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const buildChart = useCallback(() => {
|
||||
if (lineChartRef.current !== undefined) {
|
||||
lineChartRef.current.destroy();
|
||||
}
|
||||
|
||||
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' {
|
||||
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 = {
|
||||
animate: undefined,
|
||||
title: undefined,
|
||||
@ -388,6 +200,8 @@ Graph.defaultProps = {
|
||||
dragSelectColor: undefined,
|
||||
};
|
||||
|
||||
Graph.displayName = 'Graph';
|
||||
|
||||
export default memo(Graph, (prevProps, nextProps) =>
|
||||
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 { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
interface IAxisTimeUintConfig {
|
||||
unitName: TimeUnit;
|
||||
multiplier: number;
|
||||
}
|
||||
|
||||
interface IAxisTimeConfig {
|
||||
unitName: TimeUnit;
|
||||
stepSize: number;
|
||||
}
|
||||
|
||||
export interface ITimeRange {
|
||||
minTime: number | null;
|
||||
maxTime: number | null;
|
||||
}
|
||||
import { IAxisTimeConfig, IAxisTimeUintConfig, ITimeRange } from './types';
|
||||
|
||||
export const TIME_UNITS: Record<TimeUnit, TimeUnit> = {
|
||||
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_LIST_OPTIONS = 'LOGS_LIST_OPTIONS',
|
||||
TRACES_LIST_OPTIONS = 'TRACES_LIST_OPTIONS',
|
||||
GRAPH_VISIBILITY_STATES = 'GRAPH_VISIBILITY_STATES',
|
||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { StaticLineProps } from 'components/Graph';
|
||||
import { StaticLineProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
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 { GraphOnClickHandler } from 'components/Graph';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import Spinner from 'components/Spinner';
|
||||
import TimePreference from 'components/TimePreferenceDropDown';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
@ -9,15 +9,20 @@ import {
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useChartMutable } from 'hooks/useChartMutable';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
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 { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
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({
|
||||
widget,
|
||||
@ -27,6 +32,8 @@ function FullView({
|
||||
yAxisUnit,
|
||||
onDragSelect,
|
||||
isDependedDataLoaded = false,
|
||||
graphsVisibilityStates,
|
||||
onToggleModelHandler,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
@ -39,6 +46,22 @@ function FullView({
|
||||
[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>({
|
||||
name: getSelectedTime()?.name || '',
|
||||
enum: widget?.timePreferance || 'GLOBAL_TIME',
|
||||
@ -78,7 +101,11 @@ function FullView({
|
||||
[response],
|
||||
);
|
||||
|
||||
if (response.status === 'idle' || response.status === 'loading') {
|
||||
const isGraphLegendToggleAvailable = getIsGraphLegendToggleAvailable(
|
||||
widget.panelTypes,
|
||||
);
|
||||
|
||||
if (response.isFetching) {
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
}
|
||||
|
||||
@ -101,33 +128,35 @@ function FullView({
|
||||
</TimeContainer>
|
||||
)}
|
||||
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={chartDataSet}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={widget.title}
|
||||
onClickHandler={onClickHandler}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
panelData={response.data?.payload.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
/>
|
||||
<GraphContainer isGraphLegendToggleAvailable={isGraphLegendToggleAvailable}>
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
data={chartDataSet}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={widget.title}
|
||||
onClickHandler={onClickHandler}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
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 = {
|
||||
fullViewOptions: undefined,
|
||||
onClickHandler: undefined,
|
||||
@ -136,4 +165,6 @@ FullView.defaultProps = {
|
||||
isDependedDataLoaded: undefined,
|
||||
};
|
||||
|
||||
FullView.displayName = 'FullView';
|
||||
|
||||
export default FullView;
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||
|
||||
import { GraphContainerProps } from './types';
|
||||
|
||||
interface Props {
|
||||
$panelType: PANEL_TYPES;
|
||||
}
|
||||
@ -22,3 +25,36 @@ export const TimeContainer = styled.div<Props>`
|
||||
`
|
||||
: 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 { GraphOnClickHandler } from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { UpdateDashboard } from 'container/GridGraphLayout/utils';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import usePreviousValue from 'hooks/usePreviousValue';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import history from 'lib/history';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import {
|
||||
DeleteWidget,
|
||||
DeleteWidgetProps,
|
||||
} from 'store/actions/dashboard/deleteWidget';
|
||||
import { useSelector } from 'react-redux';
|
||||
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 { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import {
|
||||
getSelectedDashboard,
|
||||
getSelectedDashboardVariable,
|
||||
} from 'utils/dashboard/selectedDashboard';
|
||||
import { v4 } from 'uuid';
|
||||
import { getSelectedDashboardVariable } from 'utils/dashboard/selectedDashboard';
|
||||
|
||||
import { LayoutProps } from '..';
|
||||
import EmptyWidget from '../EmptyWidget';
|
||||
import WidgetHeader from '../WidgetHeader';
|
||||
import FullView from './FullView';
|
||||
import { FullViewContainer, Modal } from './styles';
|
||||
import { GridCardGraphProps } from './types';
|
||||
import WidgetGraphComponent from './WidgetGraphComponent';
|
||||
|
||||
function GridCardGraph({
|
||||
widget,
|
||||
deleteWidget,
|
||||
name,
|
||||
yAxisUnit,
|
||||
layout = [],
|
||||
@ -72,27 +41,16 @@ function GridCardGraph({
|
||||
initialInView: false,
|
||||
});
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
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<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||
(state) => state.dashboards,
|
||||
);
|
||||
|
||||
const selectedDashboard = getSelectedDashboard(dashboards);
|
||||
const variables = getSelectedDashboardVariable(dashboards);
|
||||
|
||||
const updatedQuery = useStepInterval(widget?.query);
|
||||
@ -142,153 +100,31 @@ function GridCardGraph({
|
||||
|
||||
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);
|
||||
|
||||
if (queryResponse.isRefetching) {
|
||||
return <Spinner height="20vh" tip="Loading..." />;
|
||||
}
|
||||
|
||||
if (queryResponse.isError && !isEmptyLayout) {
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
{getModals()}
|
||||
{!isEmpty(widget) && prevChartDataSetRef && (
|
||||
<>
|
||||
<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={prevChartDataSetRef}
|
||||
isStacked={widget?.isStacked}
|
||||
opacity={widget?.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onClickHandler={onClickHandler}
|
||||
panelData={[]}
|
||||
query={widget.query}
|
||||
/>
|
||||
</>
|
||||
<WidgetGraphComponent
|
||||
enableModel
|
||||
enableWidgetHeader
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={prevChartDataSetRef}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
layout={layout}
|
||||
setLayout={setLayout}
|
||||
allowClone={allowClone}
|
||||
allowDelete={allowDelete}
|
||||
allowEdit={allowEdit}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
@ -298,35 +134,22 @@ function GridCardGraph({
|
||||
return (
|
||||
<span ref={graphRef}>
|
||||
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
|
||||
<>
|
||||
<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={prevChartDataSetRef}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onClickHandler={onClickHandler}
|
||||
panelData={[]}
|
||||
query={widget.query}
|
||||
/>
|
||||
</>
|
||||
<WidgetGraphComponent
|
||||
enableModel={false}
|
||||
enableWidgetHeader
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={prevChartDataSetRef}
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
layout={layout}
|
||||
setLayout={setLayout}
|
||||
allowClone={allowClone}
|
||||
allowDelete={allowDelete}
|
||||
allowEdit={allowEdit}
|
||||
onClickHandler={onClickHandler}
|
||||
/>
|
||||
) : (
|
||||
<Spinner height="20vh" tip="Loading..." />
|
||||
)}
|
||||
@ -335,54 +158,21 @@ function GridCardGraph({
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
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()}
|
||||
|
||||
<span ref={graphRef}>
|
||||
{!isEmpty(widget) && !!queryResponse.data?.payload && (
|
||||
<GridPanelSwitch
|
||||
panelType={widget.panelTypes}
|
||||
<WidgetGraphComponent
|
||||
enableModel={!isEmptyLayout}
|
||||
enableWidgetHeader={!isEmptyLayout}
|
||||
widget={widget}
|
||||
queryResponse={queryResponse}
|
||||
errorMessage={errorMessage}
|
||||
data={chartData}
|
||||
isStacked={widget.isStacked}
|
||||
opacity={widget.opacity}
|
||||
title={' '} // `empty title to accommodate absolutely positioned widget header
|
||||
name={name}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onDragSelect={onDragSelect}
|
||||
onClickHandler={onClickHandler}
|
||||
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
|
||||
query={widget.query}
|
||||
allowClone={allowClone}
|
||||
allowDelete={allowDelete}
|
||||
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 = {
|
||||
onDragSelect: undefined,
|
||||
onClickHandler: undefined,
|
||||
@ -422,10 +190,4 @@ GridCardGraph.defaultProps = {
|
||||
isQueryEnabled: true,
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(GridCardGraph));
|
||||
export default 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 } from 'constants/queryBuilder';
|
||||
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';
|
||||
|
||||
function GridPanelSwitch({
|
||||
panelType,
|
||||
data,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
panelData,
|
||||
query,
|
||||
}: GridPanelSwitchProps): JSX.Element | null {
|
||||
const currentProps: PropsTypePropsMap = useMemo(() => {
|
||||
const result: PropsTypePropsMap = {
|
||||
[PANEL_TYPES.TIME_SERIES]: {
|
||||
type: 'line',
|
||||
data,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
},
|
||||
[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,
|
||||
};
|
||||
const GridPanelSwitch = forwardRef<
|
||||
ToggleGraphProps | undefined,
|
||||
GridPanelSwitchProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
panelType,
|
||||
data,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
panelData,
|
||||
query,
|
||||
},
|
||||
ref,
|
||||
): JSX.Element | null => {
|
||||
const currentProps: PropsTypePropsMap = useMemo(() => {
|
||||
const result: PropsTypePropsMap = {
|
||||
[PANEL_TYPES.TIME_SERIES]: {
|
||||
type: 'line',
|
||||
data,
|
||||
title,
|
||||
isStacked,
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
onDragSelect,
|
||||
ref,
|
||||
},
|
||||
[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;
|
||||
}, [
|
||||
data,
|
||||
isStacked,
|
||||
name,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
staticLine,
|
||||
title,
|
||||
yAxisUnit,
|
||||
panelData,
|
||||
query,
|
||||
]);
|
||||
return result;
|
||||
}, [
|
||||
data,
|
||||
isStacked,
|
||||
name,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
staticLine,
|
||||
title,
|
||||
yAxisUnit,
|
||||
panelData,
|
||||
query,
|
||||
ref,
|
||||
]);
|
||||
|
||||
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
|
||||
PropsTypePropsMap[typeof panelType]
|
||||
>;
|
||||
const componentProps = useMemo(() => currentProps[panelType], [
|
||||
panelType,
|
||||
currentProps,
|
||||
]);
|
||||
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
|
||||
PropsTypePropsMap[typeof panelType]
|
||||
>;
|
||||
const componentProps = useMemo(() => currentProps[panelType], [
|
||||
panelType,
|
||||
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
|
||||
return <Component {...componentProps} />;
|
||||
}
|
||||
GridPanelSwitch.displayName = 'GridPanelSwitch';
|
||||
|
||||
export default memo(GridPanelSwitch);
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
GraphOnClickHandler,
|
||||
GraphProps,
|
||||
StaticLineProps,
|
||||
} from 'components/Graph';
|
||||
} from 'components/Graph/types';
|
||||
import { GridTableComponentProps } from 'container/GridTableComponent/types';
|
||||
import { GridValueComponentProps } from 'container/GridValueComponent/types';
|
||||
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 {
|
||||
datasets: alldata.map((e, index) => {
|
||||
const datasetBaseConfig = {
|
||||
index,
|
||||
label: allLabels[index],
|
||||
borderColor: colors[index % colors.length] || 'red',
|
||||
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"
|
||||
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:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
|
||||
|
Loading…
x
Reference in New Issue
Block a user