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:
Rajat Dabade 2023-08-02 20:41:09 +05:30 committed by GitHub
parent 668f0c6e2b
commit b339f0509b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1865 additions and 704 deletions

View File

@ -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",

View File

@ -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();
};

View File

@ -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),
);

View 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;
}

View 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;
}
}
},
});

View File

@ -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',

View File

@ -0,0 +1,4 @@
export enum Events {
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
}

View File

@ -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',
}

View File

@ -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';

View File

@ -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,
);

View File

@ -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;

View File

@ -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[];
}

View File

@ -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}
/>
),
});

View File

@ -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;
}

View File

@ -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;

View File

@ -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,
};

View File

@ -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;

View File

@ -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};
`;

View File

@ -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;
}

View 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}`;
};

View 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));
});
});

View File

@ -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,
),
);

View File

@ -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],
},
],
};

View File

@ -0,0 +1,12 @@
import { LegendEntryProps } from '../FullView/types';
export const mocklegendEntryResult: LegendEntryProps[] = [
{
label: 'customer',
show: true,
},
{
label: 'demo-app',
show: false,
},
];

View File

@ -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);

View 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>;
}

View 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);
});
};

View File

@ -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);

View File

@ -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';

View 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;
}

View File

@ -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,

View File

@ -0,0 +1,3 @@
import EventEmitter from 'eventemitter3';
export const eventEmitter = new EventEmitter();

View File

@ -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"