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 <palashgdev@gmail.com>
Co-authored-by: Ankit Nayan <ankit@signoz.io>
This commit is contained in:
volodfast 2023-01-17 13:30:34 +02:00 committed by GitHub
parent 153e859ac3
commit 1e39131c38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 611 additions and 25 deletions

View File

@ -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<DragSelectPluginOptions> = {
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<DragSelectPluginOptions>;
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;
};

View File

@ -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<IntersectionCursorPluginOptions> = {
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<IntersectionCursorPluginOptions>;
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;
};

View File

@ -0,0 +1,20 @@
import { ChartEvent } from 'chart.js';
export type ChartEventHandler = (ev: ChartEvent | MouseEvent) => void;
export function mergeDefaultOptions<T extends Record<string, unknown>>(
options: T,
defaultOptions: Required<T>,
): Required<T> {
const sanitizedOptions = { ...options };
Object.keys(options).forEach((key) => {
if (sanitizedOptions[key as keyof T] === undefined) {
delete sanitizedOptions[key as keyof T];
}
});
return {
...defaultOptions,
...sanitizedOptions,
};
}

View File

@ -28,7 +28,19 @@ import React, { useCallback, useEffect, useRef } from 'react';
import { hasData } from './hasData'; import { hasData } from './hasData';
import { legend } from './Plugin'; import { legend } from './Plugin';
import {
createDragSelectPlugin,
createDragSelectPluginOptions,
dragSelectPluginId,
DragSelectPluginOptions,
} from './Plugin/DragSelect';
import { emptyGraph } from './Plugin/EmptyGraph'; import { emptyGraph } from './Plugin/EmptyGraph';
import {
createIntersectionCursorPlugin,
createIntersectionCursorPluginOptions,
intersectionCursorPluginId,
IntersectionCursorPluginOptions,
} from './Plugin/IntersectionCursor';
import { LegendsContainer } from './styles'; import { LegendsContainer } from './styles';
import { useXAxisTimeUnit } from './xAxisConfig'; import { useXAxisTimeUnit } from './xAxisConfig';
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig'; import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
@ -64,6 +76,8 @@ function Graph({
forceReRender, forceReRender,
staticLine, staticLine,
containerHeight, containerHeight,
onDragSelect,
dragSelectColor,
}: GraphProps): JSX.Element { }: GraphProps): JSX.Element {
const chartRef = useRef<HTMLCanvasElement>(null); const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -91,7 +105,7 @@ function Graph({
} }
if (chartRef.current !== null) { if (chartRef.current !== null) {
const options: ChartOptions = { const options: CustomChartOptions = {
animation: { animation: {
duration: animate ? 200 : 0, duration: animate ? 200 : 0,
}, },
@ -148,6 +162,15 @@ function Graph({
}, },
}, },
}, },
[dragSelectPluginId]: createDragSelectPluginOptions(
!!onDragSelect,
onDragSelect,
dragSelectColor,
),
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
!!onDragSelect,
currentTheme === 'dark' ? 'white' : 'black',
),
}, },
layout: { layout: {
padding: 0, padding: 0,
@ -211,7 +234,13 @@ function Graph({
const chartHasData = hasData(data); const chartHasData = hasData(data);
const chartPlugins = []; 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)); chartPlugins.push(legend(name, data.datasets.length > 3));
lineChartRef.current = new Chart(chartRef.current, { lineChartRef.current = new Chart(chartRef.current, {
@ -234,6 +263,9 @@ function Graph({
yAxisUnit, yAxisUnit,
onClickHandler, onClickHandler,
staticLine, staticLine,
onDragSelect,
dragSelectColor,
currentTheme,
]); ]);
useEffect(() => { useEffect(() => {
@ -248,6 +280,13 @@ function Graph({
); );
} }
type CustomChartOptions = ChartOptions & {
plugins: {
[dragSelectPluginId]: DragSelectPluginOptions | false;
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
};
};
interface GraphProps { interface GraphProps {
animate?: boolean; animate?: boolean;
type: ChartType; type: ChartType;
@ -260,6 +299,8 @@ interface GraphProps {
forceReRender?: boolean | null | number; forceReRender?: boolean | null | number;
staticLine?: StaticLineProps | undefined; staticLine?: StaticLineProps | undefined;
containerHeight?: string | number; containerHeight?: string | number;
onDragSelect?: (start: number, end: number) => void;
dragSelectColor?: string;
} }
export interface StaticLineProps { export interface StaticLineProps {
@ -287,5 +328,7 @@ Graph.defaultProps = {
forceReRender: undefined, forceReRender: undefined,
staticLine: undefined, staticLine: undefined,
containerHeight: '85%', containerHeight: '85%',
onDragSelect: undefined,
dragSelectColor: undefined,
}; };
export default Graph; export default Graph;

View File

@ -19,6 +19,7 @@ function GridGraphComponent({
name, name,
yAxisUnit, yAxisUnit,
staticLine, staticLine,
onDragSelect,
}: GridGraphComponentProps): JSX.Element | null { }: GridGraphComponentProps): JSX.Element | null {
const location = history.location.pathname; const location = history.location.pathname;
@ -38,6 +39,7 @@ function GridGraphComponent({
name, name,
yAxisUnit, yAxisUnit,
staticLine, staticLine,
onDragSelect,
}} }}
/> />
); );
@ -85,6 +87,7 @@ export interface GridGraphComponentProps {
name: string; name: string;
yAxisUnit?: string; yAxisUnit?: string;
staticLine?: StaticLineProps; staticLine?: StaticLineProps;
onDragSelect?: (start: number, end: number) => void;
} }
GridGraphComponent.defaultProps = { GridGraphComponent.defaultProps = {
@ -94,6 +97,7 @@ GridGraphComponent.defaultProps = {
onClickHandler: undefined, onClickHandler: undefined,
yAxisUnit: undefined, yAxisUnit: undefined,
staticLine: undefined, staticLine: undefined,
onDragSelect: undefined,
}; };
export default GridGraphComponent; export default GridGraphComponent;

View File

@ -27,6 +27,7 @@ function FullView({
onClickHandler, onClickHandler,
name, name,
yAxisUnit, yAxisUnit,
onDragSelect,
}: FullViewProps): JSX.Element { }: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector< const { selectedTime: globalSelectedTime } = useSelector<
AppState, AppState,
@ -102,6 +103,7 @@ function FullView({
onClickHandler, onClickHandler,
name, name,
yAxisUnit, yAxisUnit,
onDragSelect,
}} }}
/> />
</> </>
@ -114,12 +116,14 @@ interface FullViewProps {
onClickHandler?: GraphOnClickHandler; onClickHandler?: GraphOnClickHandler;
name: string; name: string;
yAxisUnit?: string; yAxisUnit?: string;
onDragSelect?: (start: number, end: number) => void;
} }
FullView.defaultProps = { FullView.defaultProps = {
fullViewOptions: undefined, fullViewOptions: undefined,
onClickHandler: undefined, onClickHandler: undefined,
yAxisUnit: undefined, yAxisUnit: undefined,
onDragSelect: undefined,
}; };
export default FullView; export default FullView;

View File

@ -30,6 +30,7 @@ function FullView({
onClickHandler, onClickHandler,
name, name,
yAxisUnit, yAxisUnit,
onDragSelect,
}: FullViewProps): JSX.Element { }: FullViewProps): JSX.Element {
const { minTime, maxTime, selectedTime: globalSelectedTime } = useSelector< const { minTime, maxTime, selectedTime: globalSelectedTime } = useSelector<
AppState, AppState,
@ -166,6 +167,7 @@ function FullView({
onClickHandler, onClickHandler,
name, name,
yAxisUnit, yAxisUnit,
onDragSelect,
}} }}
/> />
</> </>
@ -178,12 +180,14 @@ interface FullViewProps {
onClickHandler?: GraphOnClickHandler; onClickHandler?: GraphOnClickHandler;
name: string; name: string;
yAxisUnit?: string; yAxisUnit?: string;
onDragSelect?: (start: number, end: number) => void;
} }
FullView.defaultProps = { FullView.defaultProps = {
fullViewOptions: undefined, fullViewOptions: undefined,
onClickHandler: undefined, onClickHandler: undefined,
yAxisUnit: undefined, yAxisUnit: undefined,
onDragSelect: undefined,
}; };
export default FullView; export default FullView;

View File

@ -35,6 +35,7 @@ function GridCardGraph({
yAxisUnit, yAxisUnit,
layout = [], layout = [],
setLayout, setLayout,
onDragSelect,
}: GridCardGraphProps): JSX.Element { }: GridCardGraphProps): JSX.Element {
const [state, setState] = useState<GridCardGraphState>({ const [state, setState] = useState<GridCardGraphState>({
loading: true, loading: true,
@ -299,6 +300,7 @@ function GridCardGraph({
title: ' ', // empty title to accommodate absolutely positioned widget header title: ' ', // empty title to accommodate absolutely positioned widget header
name, name,
yAxisUnit, yAxisUnit,
onDragSelect,
}} }}
/> />
)} )}
@ -329,8 +331,13 @@ interface GridCardGraphProps extends DispatchProps {
layout?: Layout[]; layout?: Layout[];
// eslint-disable-next-line react/require-default-props // eslint-disable-next-line react/require-default-props
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>; setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
onDragSelect?: (start: number, end: number) => void;
} }
GridCardGraph.defaultProps = {
onDragSelect: undefined,
};
const mapDispatchToProps = ( const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>, dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({ ): DispatchProps => ({

View File

@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next';
import { connect, useDispatch, useSelector } from 'react-redux'; import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux'; import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk'; import { ThunkDispatch } from 'redux-thunk';
import { AppDispatch } from 'store';
import { UpdateTimeInterval } from 'store/actions';
import { import {
ToggleAddWidget, ToggleAddWidget,
ToggleAddWidgetProps, ToggleAddWidgetProps,
@ -63,12 +65,22 @@ function GridGraph(props: Props): JSX.Element {
const [selectedDashboard] = dashboards; const [selectedDashboard] = dashboards;
const { data } = selectedDashboard; const { data } = selectedDashboard;
const { widgets } = data; const { widgets } = data;
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch: AppDispatch = useDispatch<Dispatch<AppActions>>();
const [layouts, setLayout] = useState<LayoutProps[]>( const [layouts, setLayout] = useState<LayoutProps[]>(
getPreLayouts(widgets, selectedDashboard.data.layout || []), 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(() => { useEffect(() => {
(async (): Promise<void> => { (async (): Promise<void> => {
if (!isAddWidget) { if (!isAddWidget) {
@ -182,13 +194,14 @@ function GridGraph(props: Props): JSX.Element {
yAxisUnit={currentWidget?.yAxisUnit} yAxisUnit={currentWidget?.yAxisUnit}
layout={layout} layout={layout}
setLayout={setLayout} setLayout={setLayout}
onDragSelect={onDragSelect}
/> />
), ),
}; };
}), }),
); );
}, },
[widgets], [widgets, onDragSelect],
); );
const onEmptyWidgetHandler = useCallback(async () => { const onEmptyWidgetHandler = useCallback(async () => {

View File

@ -8,9 +8,10 @@ import { colors } from 'lib/getRandomColor';
import history from 'lib/history'; import history from 'lib/history';
import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes'; import { convertRawQueriesToTraceSelectedTags } from 'lib/resourceAttributes';
import { escapeRegExp } from 'lodash-es'; import { escapeRegExp } from 'lodash-es';
import React, { useMemo, useRef } from 'react'; import React, { useCallback, useMemo, useRef } from 'react';
import { useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { PromQLWidgets } from 'types/api/dashboard/getAll'; import { PromQLWidgets } from 'types/api/dashboard/getAll';
import MetricReducer from 'types/reducer/metrics'; import MetricReducer from 'types/reducer/metrics';
@ -22,6 +23,7 @@ import { Button } from './styles';
function Application({ getWidget }: DashboardProps): JSX.Element { function Application({ getWidget }: DashboardProps): JSX.Element {
const { servicename } = useParams<{ servicename?: string }>(); const { servicename } = useParams<{ servicename?: string }>();
const selectedTimeStamp = useRef(0); const selectedTimeStamp = useRef(0);
const dispatch = useDispatch();
const { const {
topOperations, 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 onErrorTrackHandler = (timestamp: number): void => {
const currentTime = timestamp; const currentTime = timestamp;
const tPlusOne = timestamp + 1 * 60 * 1000; const tPlusOne = timestamp + 1 * 60 * 1000;
@ -173,6 +185,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
}), }),
}} }}
yAxisUnit="ms" yAxisUnit="ms"
onDragSelect={onDragSelect}
/> />
</GraphContainer> </GraphContainer>
</Card> </Card>
@ -205,6 +218,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
}, },
])} ])}
yAxisUnit="ops" yAxisUnit="ops"
onDragSelect={onDragSelect}
/> />
</GraphContainer> </GraphContainer>
</Card> </Card>
@ -239,6 +253,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
}, },
])} ])}
yAxisUnit="%" yAxisUnit="%"
onDragSelect={onDragSelect}
/> />
</GraphContainer> </GraphContainer>
</Card> </Card>

View File

@ -60,9 +60,6 @@ function DateTimeSelection({
searchStartTime, searchStartTime,
]); ]);
const [startTime, setStartTime] = useState<Dayjs>();
const [endTime, setEndTime] = useState<Dayjs>();
const [options, setOptions] = useState(getOptions(location.pathname)); const [options, setOptions] = useState(getOptions(location.pathname));
const [refreshButtonHidden, setRefreshButtonHidden] = useState<boolean>(false); const [refreshButtonHidden, setRefreshButtonHidden] = useState<boolean>(false);
const [customDateTimeVisible, setCustomDTPickerVisible] = useState<boolean>( const [customDateTimeVisible, setCustomDTPickerVisible] = useState<boolean>(
@ -108,10 +105,6 @@ function DateTimeSelection({
return defaultSelectedOption; return defaultSelectedOption;
}; };
const [selectedTimeInterval, setSelectedTimeInterval] = useState<Time>(
getDefaultTime(location.pathname),
);
const updateLocalStorageForRoutes = (value: Time): void => { const updateLocalStorageForRoutes = (value: Time): void => {
const preRoutes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION); const preRoutes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
if (preRoutes !== null) { if (preRoutes !== null) {
@ -133,7 +126,7 @@ function DateTimeSelection({
const currentTime = dayjs(); const currentTime = dayjs();
const lastRefresh = dayjs( const lastRefresh = dayjs(
selectedTimeInterval === 'custom' ? minTime / 1000000 : maxTime / 1000000, selectedTime === 'custom' ? minTime / 1000000 : maxTime / 1000000,
); );
const secondsDiff = currentTime.diff(lastRefresh, 'seconds'); const secondsDiff = currentTime.diff(lastRefresh, 'seconds');
@ -160,13 +153,11 @@ function DateTimeSelection({
} }
return `Last refresh - ${secondsDiff} sec ago`; return `Last refresh - ${secondsDiff} sec ago`;
}, [maxTime, minTime, selectedTimeInterval]); }, [maxTime, minTime, selectedTime]);
const onSelectHandler = (value: Time): void => { const onSelectHandler = (value: Time): void => {
if (value !== 'custom') { if (value !== 'custom') {
updateTimeInterval(value); updateTimeInterval(value);
const selectedLabel = getInputLabel(undefined, undefined, value);
setSelectedTimeInterval(selectedLabel as Time);
updateLocalStorageForRoutes(value); updateLocalStorageForRoutes(value);
if (refreshButtonHidden) { if (refreshButtonHidden) {
setRefreshButtonHidden(false); setRefreshButtonHidden(false);
@ -178,7 +169,7 @@ function DateTimeSelection({
}; };
const onRefreshHandler = (): void => { const onRefreshHandler = (): void => {
onSelectHandler(selectedTimeInterval); onSelectHandler(selectedTime);
onLastRefreshHandler(); onLastRefreshHandler();
}; };
@ -186,9 +177,6 @@ function DateTimeSelection({
if (dateTimeRange !== null) { if (dateTimeRange !== null) {
const [startTimeMoment, endTimeMoment] = dateTimeRange; const [startTimeMoment, endTimeMoment] = dateTimeRange;
if (startTimeMoment && endTimeMoment) { if (startTimeMoment && endTimeMoment) {
setSelectedTimeInterval('custom');
setStartTime(startTimeMoment);
setEndTime(endTimeMoment);
setCustomDTPickerVisible(false); setCustomDTPickerVisible(false);
updateTimeInterval('custom', [ updateTimeInterval('custom', [
startTimeMoment?.toDate().getTime() || 0, startTimeMoment?.toDate().getTime() || 0,
@ -239,9 +227,6 @@ function DateTimeSelection({
const [preStartTime = 0, preEndTime = 0] = getTime() || []; const [preStartTime = 0, preEndTime = 0] = getTime() || [];
setStartTime(dayjs(preStartTime));
setEndTime(dayjs(preEndTime));
setRefreshButtonHidden(updatedTime === 'custom'); setRefreshButtonHidden(updatedTime === 'custom');
updateTimeInterval(updatedTime, [preStartTime, preEndTime]); updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
@ -266,7 +251,11 @@ function DateTimeSelection({
<FormContainer> <FormContainer>
<DefaultSelect <DefaultSelect
onSelect={(value: unknown): void => onSelectHandler(value as Time)} onSelect={(value: unknown): void => onSelectHandler(value as Time)}
value={getInputLabel(startTime, endTime, selectedTime)} value={getInputLabel(
dayjs(minTime / 1000000),
dayjs(maxTime / 1000000),
selectedTime,
)}
data-testid="dropDown" data-testid="dropDown"
> >
{options.map(({ value, label }) => ( {options.map(({ value, label }) => (

View File

@ -18,6 +18,8 @@ const store = createStore(
), ),
); );
export type AppDispatch = typeof store.dispatch;
if (window !== undefined) { if (window !== undefined) {
window.store = store; window.store = store;
} }