From 1e39131c38108eb7ebf12cc19f91464a278db53d Mon Sep 17 00:00:00 2001 From: volodfast Date: Tue, 17 Jan 2023 13:30:34 +0200 Subject: [PATCH] feat: drag select timeframe on charts (#2018) * feat: add drag select functionality to chart * fix: use redux stored values for time frame selection * fix: ignore clicks on chart without dragging * feat: add intersection cursor to chart * refactor: update drag-select chart plugin * fix: respond to drag-select mouseup outside of chart * fix: remove unnecessary chart update * feat: add drag-select to dashboard charts * refactor: add util functions to create custom plugin options * fix: enable custom chart plugins Co-authored-by: Palash Gupta Co-authored-by: Ankit Nayan --- .../src/components/Graph/Plugin/DragSelect.ts | 321 ++++++++++++++++++ .../Graph/Plugin/IntersectionCursor.ts | 164 +++++++++ frontend/src/components/Graph/Plugin/utils.ts | 20 ++ frontend/src/components/Graph/index.tsx | 47 ++- .../container/GridGraphComponent/index.tsx | 4 + .../Graph/FullView/index.metricsBuilder.tsx | 4 + .../GridGraphLayout/Graph/FullView/index.tsx | 4 + .../container/GridGraphLayout/Graph/index.tsx | 7 + .../src/container/GridGraphLayout/index.tsx | 17 +- .../MetricsApplication/Tabs/Overview.tsx | 19 +- .../TopNav/DateTimeSelection/index.tsx | 27 +- frontend/src/store/index.ts | 2 + 12 files changed, 611 insertions(+), 25 deletions(-) create mode 100644 frontend/src/components/Graph/Plugin/DragSelect.ts create mode 100644 frontend/src/components/Graph/Plugin/IntersectionCursor.ts create mode 100644 frontend/src/components/Graph/Plugin/utils.ts diff --git a/frontend/src/components/Graph/Plugin/DragSelect.ts b/frontend/src/components/Graph/Plugin/DragSelect.ts new file mode 100644 index 0000000000..a22c6da91d --- /dev/null +++ b/frontend/src/components/Graph/Plugin/DragSelect.ts @@ -0,0 +1,321 @@ +import { Chart, ChartTypeRegistry, Plugin } from 'chart.js'; +import * as ChartHelpers from 'chart.js/helpers'; + +// utils +import { ChartEventHandler, mergeDefaultOptions } from './utils'; + +export const dragSelectPluginId = 'drag-select-plugin'; + +type ChartDragHandlers = { + mousedown: ChartEventHandler; + mousemove: ChartEventHandler; + mouseup: ChartEventHandler; + globalMouseup: () => void; +}; + +export type DragSelectPluginOptions = { + color?: string; + onSelect?: (startValueX: number, endValueX: number) => void; +}; + +const defaultDragSelectPluginOptions: Required = { + color: 'rgba(0, 0, 0, 0.5)', + onSelect: () => {}, +}; + +export function createDragSelectPluginOptions( + isEnabled: boolean, + onSelect?: (start: number, end: number) => void, + color?: string, +): DragSelectPluginOptions | false { + if (!isEnabled) { + return false; + } + + return { + onSelect, + color, + }; +} + +function createMousedownHandler( + chart: Chart, + dragData: DragSelectData, +): ChartEventHandler { + return (ev): void => { + const { left, right } = chart.chartArea; + + let { x: startDragPositionX } = ChartHelpers.getRelativePosition(ev, chart); + + if (left > startDragPositionX) { + startDragPositionX = left; + } + + if (right < startDragPositionX) { + startDragPositionX = right; + } + + const startValuePositionX = chart.scales.x.getValueForPixel( + startDragPositionX, + ); + + dragData.onDragStart(startDragPositionX, startValuePositionX); + }; +} + +function createMousemoveHandler( + chart: Chart, + dragData: DragSelectData, +): ChartEventHandler { + return (ev): void => { + if (!dragData.isMouseDown) { + return; + } + + const { left, right } = chart.chartArea; + + let { x: dragPositionX } = ChartHelpers.getRelativePosition(ev, chart); + + if (left > dragPositionX) { + dragPositionX = left; + } + + if (right < dragPositionX) { + dragPositionX = right; + } + + const valuePositionX = chart.scales.x.getValueForPixel(dragPositionX); + + dragData.onDrag(dragPositionX, valuePositionX); + chart.update('none'); + }; +} + +function createMouseupHandler( + chart: Chart, + options: DragSelectPluginOptions, + dragData: DragSelectData, +): ChartEventHandler { + return (ev): void => { + const { left, right } = chart.chartArea; + + let { x: endRelativePostionX } = ChartHelpers.getRelativePosition(ev, chart); + + if (left > endRelativePostionX) { + endRelativePostionX = left; + } + + if (right < endRelativePostionX) { + endRelativePostionX = right; + } + + const endValuePositionX = chart.scales.x.getValueForPixel( + endRelativePostionX, + ); + + dragData.onDragEnd(endRelativePostionX, endValuePositionX); + + chart.update('none'); + + if ( + typeof options.onSelect === 'function' && + typeof dragData.startValuePositionX === 'number' && + typeof dragData.endValuePositionX === 'number' + ) { + const start = Math.min( + dragData.startValuePositionX, + dragData.endValuePositionX, + ); + const end = Math.max( + dragData.startValuePositionX, + dragData.endValuePositionX, + ); + + options.onSelect(start, end); + } + }; +} + +function createGlobalMouseupHandler( + options: DragSelectPluginOptions, + dragData: DragSelectData, +): () => void { + return (): void => { + const { isDragging, endRelativePixelPositionX, endValuePositionX } = dragData; + + if (!isDragging) { + return; + } + + dragData.onDragEnd( + endRelativePixelPositionX as number, + endValuePositionX as number, + ); + + if ( + typeof options.onSelect === 'function' && + typeof dragData.startValuePositionX === 'number' && + typeof dragData.endValuePositionX === 'number' + ) { + const start = Math.min( + dragData.startValuePositionX, + dragData.endValuePositionX, + ); + const end = Math.max( + dragData.startValuePositionX, + dragData.endValuePositionX, + ); + + options.onSelect(start, end); + } + }; +} + +class DragSelectData { + public isDragging = false; + + public isMouseDown = false; + + public startRelativePixelPositionX: number | null = null; + + public startValuePositionX: number | null | undefined = null; + + public endRelativePixelPositionX: number | null = null; + + public endValuePositionX: number | null | undefined = null; + + public initialize(): void { + this.isDragging = false; + this.isMouseDown = false; + this.startRelativePixelPositionX = null; + this.startValuePositionX = null; + this.endRelativePixelPositionX = null; + this.endValuePositionX = null; + } + + public onDragStart( + startRelativePixelPositionX: number, + startValuePositionX: number | undefined, + ): void { + this.isDragging = false; + this.isMouseDown = true; + this.startRelativePixelPositionX = startRelativePixelPositionX; + this.startValuePositionX = startValuePositionX; + this.endRelativePixelPositionX = null; + this.endValuePositionX = null; + } + + public onDrag( + endRelativePixelPositionX: number, + endValuePositionX: number | undefined, + ): void { + this.isDragging = true; + this.endRelativePixelPositionX = endRelativePixelPositionX; + this.endValuePositionX = endValuePositionX; + } + + public onDragEnd( + endRelativePixelPositionX: number, + endValuePositionX: number | undefined, + ): void { + if (!this.isDragging) { + this.initialize(); + return; + } + + this.isDragging = false; + this.isMouseDown = false; + this.endRelativePixelPositionX = endRelativePixelPositionX; + this.endValuePositionX = endValuePositionX; + } +} + +export const createDragSelectPlugin = (): Plugin< + keyof ChartTypeRegistry, + DragSelectPluginOptions +> => { + const dragData = new DragSelectData(); + let pluginOptions: Required; + + const handlers: ChartDragHandlers = { + mousedown: () => {}, + mousemove: () => {}, + mouseup: () => {}, + globalMouseup: () => {}, + }; + + const dragSelectPlugin: Plugin< + keyof ChartTypeRegistry, + DragSelectPluginOptions + > = { + id: dragSelectPluginId, + start: (chart: Chart, _, passedOptions) => { + pluginOptions = mergeDefaultOptions( + passedOptions, + defaultDragSelectPluginOptions, + ); + + const { canvas } = chart; + + dragData.initialize(); + + const mousedownHandler = createMousedownHandler(chart, dragData); + const mousemoveHandler = createMousemoveHandler(chart, dragData); + const mouseupHandler = createMouseupHandler(chart, pluginOptions, dragData); + const globalMouseupHandler = createGlobalMouseupHandler( + pluginOptions, + dragData, + ); + + canvas.addEventListener('mousedown', mousedownHandler, { passive: true }); + canvas.addEventListener('mousemove', mousemoveHandler, { passive: true }); + canvas.addEventListener('mouseup', mouseupHandler, { passive: true }); + document.addEventListener('mouseup', globalMouseupHandler, { + passive: true, + }); + + handlers.mousedown = mousedownHandler; + handlers.mousemove = mousemoveHandler; + handlers.mouseup = mouseupHandler; + handlers.globalMouseup = globalMouseupHandler; + }, + beforeDestroy: (chart: Chart) => { + const { canvas } = chart; + + if (!canvas) { + return; + } + + canvas.removeEventListener('mousedown', handlers.mousedown); + canvas.removeEventListener('mousemove', handlers.mousemove); + canvas.removeEventListener('mouseup', handlers.mouseup); + document.removeEventListener('mouseup', handlers.globalMouseup); + }, + afterDatasetsDraw: (chart: Chart) => { + const { + startRelativePixelPositionX, + endRelativePixelPositionX, + isDragging, + } = dragData; + + if (startRelativePixelPositionX && endRelativePixelPositionX && isDragging) { + const left = Math.min( + startRelativePixelPositionX, + endRelativePixelPositionX, + ); + const right = Math.max( + startRelativePixelPositionX, + endRelativePixelPositionX, + ); + const top = chart.chartArea.top - 5; + const bottom = chart.chartArea.bottom + 5; + + /* eslint-disable-next-line no-param-reassign */ + chart.ctx.fillStyle = pluginOptions.color; + chart.ctx.fillRect(left, top, right - left, bottom - top); + } + }, + }; + + return dragSelectPlugin; +}; diff --git a/frontend/src/components/Graph/Plugin/IntersectionCursor.ts b/frontend/src/components/Graph/Plugin/IntersectionCursor.ts new file mode 100644 index 0000000000..99b4e50011 --- /dev/null +++ b/frontend/src/components/Graph/Plugin/IntersectionCursor.ts @@ -0,0 +1,164 @@ +import { Chart, ChartEvent, ChartTypeRegistry, Plugin } from 'chart.js'; +import * as ChartHelpers from 'chart.js/helpers'; + +// utils +import { ChartEventHandler, mergeDefaultOptions } from './utils'; + +export const intersectionCursorPluginId = 'intersection-cursor-plugin'; + +export type IntersectionCursorPluginOptions = { + color?: string; + dashSize?: number; + gapSize?: number; +}; + +export const defaultIntersectionCursorPluginOptions: Required = { + color: 'white', + dashSize: 3, + gapSize: 3, +}; + +export function createIntersectionCursorPluginOptions( + isEnabled: boolean, + color?: string, + dashSize?: number, + gapSize?: number, +): IntersectionCursorPluginOptions | false { + if (!isEnabled) { + return false; + } + + return { + color, + dashSize, + gapSize, + }; +} + +function createMousemoveHandler( + chart: Chart, + cursorData: IntersectionCursorData, +): ChartEventHandler { + return (ev: ChartEvent | MouseEvent): void => { + const { left, right, top, bottom } = chart.chartArea; + + let { x, y } = ChartHelpers.getRelativePosition(ev, chart); + + if (left > x) { + x = left; + } + + if (right < x) { + x = right; + } + + if (y < top) { + y = top; + } + + if (y > bottom) { + y = bottom; + } + + cursorData.onMouseMove(x, y); + }; +} + +function createMouseoutHandler( + cursorData: IntersectionCursorData, +): ChartEventHandler { + return (): void => { + cursorData.onMouseOut(); + }; +} + +class IntersectionCursorData { + public positionX: number | null | undefined; + + public positionY: number | null | undefined; + + public initialize(): void { + this.positionX = null; + this.positionY = null; + } + + public onMouseMove(x: number | undefined, y: number | undefined): void { + this.positionX = x; + this.positionY = y; + } + + public onMouseOut(): void { + this.positionX = null; + this.positionY = null; + } +} + +export const createIntersectionCursorPlugin = (): Plugin< + keyof ChartTypeRegistry, + IntersectionCursorPluginOptions +> => { + const cursorData = new IntersectionCursorData(); + let pluginOptions: Required; + + let mousemoveHandler: (ev: ChartEvent | MouseEvent) => void; + let mouseoutHandler: (ev: ChartEvent | MouseEvent) => void; + + const intersectionCursorPlugin: Plugin< + keyof ChartTypeRegistry, + IntersectionCursorPluginOptions + > = { + id: intersectionCursorPluginId, + start: (chart: Chart, _, passedOptions) => { + const { canvas } = chart; + + cursorData.initialize(); + pluginOptions = mergeDefaultOptions( + passedOptions, + defaultIntersectionCursorPluginOptions, + ); + + mousemoveHandler = createMousemoveHandler(chart, cursorData); + mouseoutHandler = createMouseoutHandler(cursorData); + + canvas.addEventListener('mousemove', mousemoveHandler, { passive: true }); + canvas.addEventListener('mouseout', mouseoutHandler, { passive: true }); + }, + beforeDestroy: (chart: Chart) => { + const { canvas } = chart; + + if (!canvas) { + return; + } + + canvas.removeEventListener('mousemove', mousemoveHandler); + canvas.removeEventListener('mouseout', mouseoutHandler); + }, + afterDatasetsDraw: (chart: Chart) => { + const { positionX, positionY } = cursorData; + + const lineDashData = [pluginOptions.dashSize, pluginOptions.gapSize]; + + if (typeof positionX === 'number' && typeof positionY === 'number') { + const { top, bottom, left, right } = chart.chartArea; + + chart.ctx.beginPath(); + /* eslint-disable-next-line no-param-reassign */ + chart.ctx.strokeStyle = pluginOptions.color; + chart.ctx.setLineDash(lineDashData); + chart.ctx.moveTo(left, positionY); + chart.ctx.lineTo(right, positionY); + chart.ctx.stroke(); + + chart.ctx.beginPath(); + chart.ctx.setLineDash(lineDashData); + /* eslint-disable-next-line no-param-reassign */ + chart.ctx.strokeStyle = pluginOptions.color; + chart.ctx.moveTo(positionX, top); + chart.ctx.lineTo(positionX, bottom); + chart.ctx.stroke(); + } + }, + }; + + return intersectionCursorPlugin; +}; diff --git a/frontend/src/components/Graph/Plugin/utils.ts b/frontend/src/components/Graph/Plugin/utils.ts new file mode 100644 index 0000000000..4260e9e147 --- /dev/null +++ b/frontend/src/components/Graph/Plugin/utils.ts @@ -0,0 +1,20 @@ +import { ChartEvent } from 'chart.js'; + +export type ChartEventHandler = (ev: ChartEvent | MouseEvent) => void; + +export function mergeDefaultOptions>( + options: T, + defaultOptions: Required, +): Required { + const sanitizedOptions = { ...options }; + Object.keys(options).forEach((key) => { + if (sanitizedOptions[key as keyof T] === undefined) { + delete sanitizedOptions[key as keyof T]; + } + }); + + return { + ...defaultOptions, + ...sanitizedOptions, + }; +} diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index 7142e71d09..6c26331e32 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -28,7 +28,19 @@ import React, { useCallback, useEffect, useRef } from 'react'; import { hasData } from './hasData'; import { legend } from './Plugin'; +import { + createDragSelectPlugin, + createDragSelectPluginOptions, + dragSelectPluginId, + DragSelectPluginOptions, +} from './Plugin/DragSelect'; import { emptyGraph } from './Plugin/EmptyGraph'; +import { + createIntersectionCursorPlugin, + createIntersectionCursorPluginOptions, + intersectionCursorPluginId, + IntersectionCursorPluginOptions, +} from './Plugin/IntersectionCursor'; import { LegendsContainer } from './styles'; import { useXAxisTimeUnit } from './xAxisConfig'; import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig'; @@ -64,6 +76,8 @@ function Graph({ forceReRender, staticLine, containerHeight, + onDragSelect, + dragSelectColor, }: GraphProps): JSX.Element { const chartRef = useRef(null); const isDarkMode = useIsDarkMode(); @@ -91,7 +105,7 @@ function Graph({ } if (chartRef.current !== null) { - const options: ChartOptions = { + const options: CustomChartOptions = { animation: { duration: animate ? 200 : 0, }, @@ -148,6 +162,15 @@ function Graph({ }, }, }, + [dragSelectPluginId]: createDragSelectPluginOptions( + !!onDragSelect, + onDragSelect, + dragSelectColor, + ), + [intersectionCursorPluginId]: createIntersectionCursorPluginOptions( + !!onDragSelect, + currentTheme === 'dark' ? 'white' : 'black', + ), }, layout: { padding: 0, @@ -211,7 +234,13 @@ function Graph({ const chartHasData = hasData(data); const chartPlugins = []; - if (!chartHasData) chartPlugins.push(emptyGraph); + 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, { @@ -234,6 +263,9 @@ function Graph({ yAxisUnit, onClickHandler, staticLine, + onDragSelect, + dragSelectColor, + currentTheme, ]); useEffect(() => { @@ -248,6 +280,13 @@ function Graph({ ); } +type CustomChartOptions = ChartOptions & { + plugins: { + [dragSelectPluginId]: DragSelectPluginOptions | false; + [intersectionCursorPluginId]: IntersectionCursorPluginOptions | false; + }; +}; + interface GraphProps { animate?: boolean; type: ChartType; @@ -260,6 +299,8 @@ interface GraphProps { forceReRender?: boolean | null | number; staticLine?: StaticLineProps | undefined; containerHeight?: string | number; + onDragSelect?: (start: number, end: number) => void; + dragSelectColor?: string; } export interface StaticLineProps { @@ -287,5 +328,7 @@ Graph.defaultProps = { forceReRender: undefined, staticLine: undefined, containerHeight: '85%', + onDragSelect: undefined, + dragSelectColor: undefined, }; export default Graph; diff --git a/frontend/src/container/GridGraphComponent/index.tsx b/frontend/src/container/GridGraphComponent/index.tsx index 3a1b84e963..f5612b0a40 100644 --- a/frontend/src/container/GridGraphComponent/index.tsx +++ b/frontend/src/container/GridGraphComponent/index.tsx @@ -19,6 +19,7 @@ function GridGraphComponent({ name, yAxisUnit, staticLine, + onDragSelect, }: GridGraphComponentProps): JSX.Element | null { const location = history.location.pathname; @@ -38,6 +39,7 @@ function GridGraphComponent({ name, yAxisUnit, staticLine, + onDragSelect, }} /> ); @@ -85,6 +87,7 @@ export interface GridGraphComponentProps { name: string; yAxisUnit?: string; staticLine?: StaticLineProps; + onDragSelect?: (start: number, end: number) => void; } GridGraphComponent.defaultProps = { @@ -94,6 +97,7 @@ GridGraphComponent.defaultProps = { onClickHandler: undefined, yAxisUnit: undefined, staticLine: undefined, + onDragSelect: undefined, }; export default GridGraphComponent; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx index 5aa298b76b..4c1912f471 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx @@ -27,6 +27,7 @@ function FullView({ onClickHandler, name, yAxisUnit, + onDragSelect, }: FullViewProps): JSX.Element { const { selectedTime: globalSelectedTime } = useSelector< AppState, @@ -102,6 +103,7 @@ function FullView({ onClickHandler, name, yAxisUnit, + onDragSelect, }} /> @@ -114,12 +116,14 @@ interface FullViewProps { onClickHandler?: GraphOnClickHandler; name: string; yAxisUnit?: string; + onDragSelect?: (start: number, end: number) => void; } FullView.defaultProps = { fullViewOptions: undefined, onClickHandler: undefined, yAxisUnit: undefined, + onDragSelect: undefined, }; export default FullView; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx index ad228f642e..fe9d5e08bc 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx @@ -30,6 +30,7 @@ function FullView({ onClickHandler, name, yAxisUnit, + onDragSelect, }: FullViewProps): JSX.Element { const { minTime, maxTime, selectedTime: globalSelectedTime } = useSelector< AppState, @@ -166,6 +167,7 @@ function FullView({ onClickHandler, name, yAxisUnit, + onDragSelect, }} /> @@ -178,12 +180,14 @@ interface FullViewProps { onClickHandler?: GraphOnClickHandler; name: string; yAxisUnit?: string; + onDragSelect?: (start: number, end: number) => void; } FullView.defaultProps = { fullViewOptions: undefined, onClickHandler: undefined, yAxisUnit: undefined, + onDragSelect: undefined, }; export default FullView; diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index 54cba3e8e6..84942ef05a 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -35,6 +35,7 @@ function GridCardGraph({ yAxisUnit, layout = [], setLayout, + onDragSelect, }: GridCardGraphProps): JSX.Element { const [state, setState] = useState({ loading: true, @@ -299,6 +300,7 @@ function GridCardGraph({ title: ' ', // empty title to accommodate absolutely positioned widget header name, yAxisUnit, + onDragSelect, }} /> )} @@ -329,8 +331,13 @@ interface GridCardGraphProps extends DispatchProps { layout?: Layout[]; // eslint-disable-next-line react/require-default-props setLayout?: React.Dispatch>; + onDragSelect?: (start: number, end: number) => void; } +GridCardGraph.defaultProps = { + onDragSelect: undefined, +}; + const mapDispatchToProps = ( dispatch: ThunkDispatch, ): DispatchProps => ({ diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx index 234b10ceb7..553632e0d6 100644 --- a/frontend/src/container/GridGraphLayout/index.tsx +++ b/frontend/src/container/GridGraphLayout/index.tsx @@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next'; import { connect, useDispatch, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; +import { AppDispatch } from 'store'; +import { UpdateTimeInterval } from 'store/actions'; import { ToggleAddWidget, ToggleAddWidgetProps, @@ -63,12 +65,22 @@ function GridGraph(props: Props): JSX.Element { const [selectedDashboard] = dashboards; const { data } = selectedDashboard; const { widgets } = data; - const dispatch = useDispatch>(); + const dispatch: AppDispatch = useDispatch>(); const [layouts, setLayout] = useState( getPreLayouts(widgets, selectedDashboard.data.layout || []), ); + const onDragSelect = useCallback( + (start: number, end: number) => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + }, + [dispatch], + ); + useEffect(() => { (async (): Promise => { if (!isAddWidget) { @@ -182,13 +194,14 @@ function GridGraph(props: Props): JSX.Element { yAxisUnit={currentWidget?.yAxisUnit} layout={layout} setLayout={setLayout} + onDragSelect={onDragSelect} /> ), }; }), ); }, - [widgets], + [widgets, onDragSelect], ); const onEmptyWidgetHandler = useCallback(async () => { diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 082e6514e6..308181b180 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -8,9 +8,10 @@ import { colors } from 'lib/getRandomColor'; import history from 'lib/history'; import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes'; import { escapeRegExp } from 'lodash-es'; -import React, { useMemo, useRef } from 'react'; -import { useSelector } from 'react-redux'; +import React, { useCallback, useMemo, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; +import { UpdateTimeInterval } from 'store/actions'; import { AppState } from 'store/reducers'; import { PromQLWidgets } from 'types/api/dashboard/getAll'; import MetricReducer from 'types/reducer/metrics'; @@ -22,6 +23,7 @@ import { Button } from './styles'; function Application({ getWidget }: DashboardProps): JSX.Element { const { servicename } = useParams<{ servicename?: string }>(); const selectedTimeStamp = useRef(0); + const dispatch = useDispatch(); const { topOperations, @@ -92,6 +94,16 @@ function Application({ getWidget }: DashboardProps): JSX.Element { } }; + const onDragSelect = useCallback( + (start: number, end: number) => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + }, + [dispatch], + ); + const onErrorTrackHandler = (timestamp: number): void => { const currentTime = timestamp; const tPlusOne = timestamp + 1 * 60 * 1000; @@ -173,6 +185,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element { }), }} yAxisUnit="ms" + onDragSelect={onDragSelect} /> @@ -205,6 +218,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element { }, ])} yAxisUnit="ops" + onDragSelect={onDragSelect} /> @@ -239,6 +253,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element { }, ])} yAxisUnit="%" + onDragSelect={onDragSelect} /> diff --git a/frontend/src/container/TopNav/DateTimeSelection/index.tsx b/frontend/src/container/TopNav/DateTimeSelection/index.tsx index 18e8dce4c8..9a18b65759 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/index.tsx +++ b/frontend/src/container/TopNav/DateTimeSelection/index.tsx @@ -60,9 +60,6 @@ function DateTimeSelection({ searchStartTime, ]); - const [startTime, setStartTime] = useState(); - const [endTime, setEndTime] = useState(); - const [options, setOptions] = useState(getOptions(location.pathname)); const [refreshButtonHidden, setRefreshButtonHidden] = useState(false); const [customDateTimeVisible, setCustomDTPickerVisible] = useState( @@ -108,10 +105,6 @@ function DateTimeSelection({ return defaultSelectedOption; }; - const [selectedTimeInterval, setSelectedTimeInterval] = useState