diff --git a/frontend/package.json b/frontend/package.json index 174fab4073..9ea7ddd00a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/Graph/Plugin/Legend.ts b/frontend/src/components/Graph/Plugin/Legend.ts index 8880657855..809e0d135c 100644 --- a/frontend/src/components/Graph/Plugin/Legend.ts +++ b/frontend/src/components/Graph/Plugin/Legend.ts @@ -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 => ({ item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex), ); + eventEmitter.emit(Events.UPDATE_GRAPH_MANAGER_TABLE, { + name: id, + index: item.datasetIndex, + }); } chart.update(); }; diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index 621f57ebf7..d1deb08c4e 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -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); - const chartRef = useRef(null); - const isDarkMode = useIsDarkMode(); +const Graph = forwardRef( + ( + { + animate = true, + data, + type, + title, + isStacked, + onClickHandler, + name, + yAxisUnit = 'short', + forceReRender, + staticLine, + containerHeight, + onDragSelect, + dragSelectColor, + }, + ref, + ): JSX.Element => { + const nearestDatasetIndex = useRef(null); + const chartRef = useRef(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(); - const getGridColor = useCallback(() => { - if (currentTheme === undefined) { - return 'rgba(231,233,237,0.1)'; - } + const lineChartRef = useRef(); - 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 ( -
- - -
- ); -} + 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 ( +
+ + +
+ ); + }, +); 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), ); diff --git a/frontend/src/components/Graph/types.ts b/frontend/src/components/Graph/types.ts new file mode 100644 index 0000000000..dcb9607a0c --- /dev/null +++ b/frontend/src/components/Graph/types.ts @@ -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; +} + +export interface IAxisTimeUintConfig { + unitName: TimeUnit; + multiplier: number; +} + +export interface IAxisTimeConfig { + unitName: TimeUnit; + stepSize: number; +} + +export interface ITimeRange { + minTime: number | null; + maxTime: number | null; +} diff --git a/frontend/src/components/Graph/utils.ts b/frontend/src/components/Graph/utils.ts new file mode 100644 index 0000000000..db30b6a8ce --- /dev/null +++ b/frontend/src/components/Graph/utils.ts @@ -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, +): 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, + 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; + } + } + }, +}); diff --git a/frontend/src/components/Graph/xAxisConfig.ts b/frontend/src/components/Graph/xAxisConfig.ts index ee794b6678..3fa0b00e08 100644 --- a/frontend/src/components/Graph/xAxisConfig.ts +++ b/frontend/src/components/Graph/xAxisConfig.ts @@ -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 = { millisecond: 'millisecond', diff --git a/frontend/src/constants/events.ts b/frontend/src/constants/events.ts new file mode 100644 index 0000000000..db4ce63d7a --- /dev/null +++ b/frontend/src/constants/events.ts @@ -0,0 +1,4 @@ +export enum Events { + UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE', + UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE', +} diff --git a/frontend/src/constants/localStorage.ts b/frontend/src/constants/localStorage.ts index 63aee6b30a..450546abb4 100644 --- a/frontend/src/constants/localStorage.ts +++ b/frontend/src/constants/localStorage.ts @@ -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', } diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index b8d66c4334..33bcfa3c37 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -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'; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/GraphManager.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/GraphManager.tsx new file mode 100644 index 0000000000..61abbf2aa6 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/GraphManager.tsx @@ -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( + localstoredVisibilityStates, + ); + + const [tableDataSet, setTableDataSet] = useState( + getDefaultTableDataSet(data), + ); + + const { notifications } = useNotifications(); + + // useEffect for updating graph visibility state on data change + useEffect(() => { + const newGraphVisibilityStates = Array(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(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): 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 ( + + + + + + + + + + + + + + + ); +} + +GraphManager.defaultProps = { + graphVisibilityStateHandler: undefined, +}; + +export default memo( + GraphManager, + (prevProps, nextProps) => + isEqual(prevProps.data, nextProps.data) && prevProps.name === nextProps.name, +); diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/CustomCheckBox.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/CustomCheckBox.tsx new file mode 100644 index 0000000000..22ae630bb8 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/CustomCheckBox.tsx @@ -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 ( + + + + ); +} + +export default CustomCheckBox; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetCheckBox.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetCheckBox.tsx new file mode 100644 index 0000000000..55485be5ad --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetCheckBox.tsx @@ -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 => ({ + render: (index: number): JSX.Element => ( + + ), +}); + +interface GetCheckBoxProps { + data: ChartData; + checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void; + graphVisibilityState: boolean[]; +} diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetLabel.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetLabel.tsx new file mode 100644 index 0000000000..7fc5da2ab7 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/TableRender/GetLabel.tsx @@ -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 => ({ + render: (label: string, _, index): JSX.Element => ( +