Compare commits

...

4 Commits

Author SHA1 Message Date
Abhi kumar
e85395acbf Merge branch 'main' into feat/cross-panel-sync 2026-02-16 20:56:31 +05:30
Abhi Kumar
401d55b5a1 fix: added fix for type in tooltipplugin test 2026-02-16 20:52:53 +05:30
Abhi Kumar
dbe7fcea00 chore: minor types fix 2026-02-16 20:51:23 +05:30
Abhi Kumar
11b8fd1d8b feat: added cross panel sync option 2026-02-16 20:47:21 +05:30
9 changed files with 157 additions and 33 deletions

View File

@@ -1,6 +1,7 @@
.overview-content {
display: flex;
flex-direction: column;
gap: 16px;
.overview-settings {
border-radius: 3px;
@@ -55,6 +56,35 @@
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
.cross-panel-sync-section {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
.cross-panel-sync-info {
display: flex;
flex-direction: column;
flex: 1;
min-width: 200px;
.cross-panel-sync-title {
color: var(--bg-vanilla-400);
font-size: 14px;
}
.cross-panel-sync-description {
color: var(--bg-vanilla-400);
font-size: 12px;
}
}
.ant-radio-group {
flex-shrink: 0;
}
}
}
.overview-settings-footer {
@@ -168,6 +198,15 @@
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-300);
}
.cross-panel-sync-section {
.cross-panel-sync-title {
color: var(--bg-ink-400);
}
.cross-panel-sync-description {
color: var(--bg-ink-300);
}
}
}
.overview-settings-footer {

View File

@@ -1,11 +1,15 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Col, Input, Select, Space, Typography } from 'antd';
import { Col, Input, Radio, Select, Space, Typography } from 'antd';
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddTags';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { isEqual } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
CROSS_PANEL_SYNC_OPTIONS,
CrossPanelSync,
} from 'types/api/dashboard/getAll';
import { Button } from './styles';
import { Base64Icons } from './utils';
@@ -21,8 +25,13 @@ function GeneralDashboardSettings(): JSX.Element {
const selectedData = selectedDashboard?.data;
const { title = '', tags = [], description = '', image = Base64Icons[0] } =
selectedData || {};
const {
title = '',
tags = [],
description = '',
image = Base64Icons[0],
crossPanelSync = 'NONE',
} = selectedData || {};
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
const [updatedTags, setUpdatedTags] = useState<string[]>(tags || []);
@@ -30,6 +39,10 @@ function GeneralDashboardSettings(): JSX.Element {
description || '',
);
const [updatedImage, setUpdatedImage] = useState<string>(image);
const [
updatedCrossPanelSync,
setUpdatedCrossPanelSync,
] = useState<CrossPanelSync>(crossPanelSync);
const [numberOfUnsavedChanges, setNumberOfUnsavedChanges] = useState<number>(
0,
);
@@ -50,6 +63,7 @@ function GeneralDashboardSettings(): JSX.Element {
tags: updatedTags,
title: updatedTitle,
image: updatedImage,
crossPanelSync: updatedCrossPanelSync,
},
},
{
@@ -65,12 +79,13 @@ function GeneralDashboardSettings(): JSX.Element {
useEffect(() => {
let numberOfUnsavedChanges = 0;
const initialValues = [title, description, tags, image];
const initialValues = [title, description, tags, image, crossPanelSync];
const updatedValues = [
updatedTitle,
updatedDescription,
updatedTags,
updatedImage,
updatedCrossPanelSync,
];
initialValues.forEach((val, index) => {
if (!isEqual(val, updatedValues[index])) {
@@ -79,21 +94,38 @@ function GeneralDashboardSettings(): JSX.Element {
});
setNumberOfUnsavedChanges(numberOfUnsavedChanges);
}, [
crossPanelSync,
description,
image,
tags,
title,
updatedCrossPanelSync,
updatedDescription,
updatedImage,
updatedTags,
updatedTitle,
]);
const crossPanelSyncOptions = useMemo(() => {
return CROSS_PANEL_SYNC_OPTIONS.map((value) => {
const sanitizedValue = value.toLowerCase();
const label =
sanitizedValue === 'none'
? 'No Sync'
: sanitizedValue.charAt(0).toUpperCase() + sanitizedValue.slice(1);
return {
label,
value,
};
});
}, []);
const discardHandler = (): void => {
setUpdatedTitle(title);
setUpdatedImage(image);
setUpdatedTags(tags);
setUpdatedDescription(description);
setUpdatedCrossPanelSync(crossPanelSync);
};
return (
@@ -156,6 +188,28 @@ function GeneralDashboardSettings(): JSX.Element {
</div>
</Space>
</Col>
<Col className="overview-settings">
<div className="cross-panel-sync-section">
<div className="cross-panel-sync-info">
<Typography className="cross-panel-sync-title">
Cross-Panel Sync
</Typography>
<Typography.Text className="cross-panel-sync-description">
Sync crosshair and tooltip across all the dashboard panels
</Typography.Text>
</div>
<Radio.Group
value={updatedCrossPanelSync}
onChange={(e): void =>
setUpdatedCrossPanelSync(e.target.value as CrossPanelSync)
}
optionType="button"
buttonStyle="solid"
options={crossPanelSyncOptions}
data-testid="cross-panel-sync"
/>
</div>
</Col>
{numberOfUnsavedChanges > 0 && (
<div className="overview-settings-footer">
<div className="unsaved">

View File

@@ -1,7 +1,7 @@
import { PrecisionOption } from 'components/Graph/types';
import { LegendConfig, TooltipRenderArgs } from 'lib/uPlotV2/components/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { CrossPanelSync } from 'types/api/dashboard/getAll';
interface BaseChartProps {
width: number;
@@ -17,7 +17,7 @@ interface BaseChartProps {
interface UPlotBasedChartProps {
config: UPlotConfigBuilder;
data: uPlot.AlignedData;
syncMode?: DashboardCursorSync;
syncMode?: CrossPanelSync;
syncKey?: string;
plotRef?: (plot: uPlot | null) => void;
onDestroy?: (plot: uPlot) => void;

View File

@@ -13,6 +13,7 @@ import { getTimeRange } from 'utils/getTimeRange';
import BarChart from '../../charts/BarChart/BarChart';
import ChartManager from '../../components/ChartManager/ChartManager';
import { usePanelContextMenu } from '../../hooks/usePanelContextMenu';
import { PanelMode } from '../types';
import { prepareBarPanelConfig, prepareBarPanelData } from './utils';
import '../Panel.styles.scss';
@@ -27,7 +28,11 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
onToggleModelHandler,
} = props;
const uPlotRef = useRef<uPlot | null>(null);
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const {
toScrollWidgetId,
setToScrollWidgetId,
selectedDashboard,
} = useDashboard();
const graphRef = useRef<HTMLDivElement>(null);
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
@@ -117,6 +122,15 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
onToggleModelHandler,
]);
const crossPanelSync = selectedDashboard?.data?.crossPanelSync ?? 'NONE';
const cursorSyncMode = useMemo(() => {
if (panelMode !== PanelMode.DASHBOARD_VIEW) {
return 'NONE';
}
return crossPanelSync;
}, [panelMode, crossPanelSync]);
const onPlotDestroy = useCallback(() => {
uPlotRef.current = null;
}, []);
@@ -137,6 +151,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
onDestroy={onPlotDestroy}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
syncMode={cursorSyncMode}
timezone={timezone.value}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}

View File

@@ -14,6 +14,7 @@ import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { prepareChartData, prepareUPlotConfig } from '../TimeSeriesPanel/utils';
import { PanelMode } from '../types';
import '../Panel.styles.scss';
@@ -26,7 +27,11 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
isFullViewMode,
onToggleModelHandler,
} = props;
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const {
toScrollWidgetId,
setToScrollWidgetId,
selectedDashboard,
} = useDashboard();
const graphRef = useRef<HTMLDivElement>(null);
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
@@ -96,6 +101,15 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
timezone,
]);
const crossPanelSync = selectedDashboard?.data?.crossPanelSync ?? 'NONE';
const cursorSyncMode = useMemo(() => {
if (panelMode !== PanelMode.DASHBOARD_VIEW) {
return 'NONE';
}
return crossPanelSync;
}, [panelMode, crossPanelSync]);
const layoutChildren = useMemo(() => {
if (!isFullViewMode) {
return null;
@@ -126,6 +140,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
}}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
syncMode={cursorSyncMode}
timezone={timezone.value}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}

View File

@@ -13,7 +13,6 @@ import {
updateWindowSize,
} from './tooltipController';
import {
DashboardCursorSync,
TooltipControllerContext,
TooltipControllerState,
TooltipLayoutInfo,
@@ -35,7 +34,7 @@ export default function TooltipPlugin({
render,
maxWidth = 300,
maxHeight = 400,
syncMode = DashboardCursorSync.None,
syncMode = 'NONE',
syncKey = '_tooltip_sync_global_',
canPinTooltip = false,
}: TooltipPluginProps): JSX.Element | null {
@@ -78,11 +77,11 @@ export default function TooltipPlugin({
// render on every mouse move.
const controller: TooltipControllerState = createInitialControllerState();
const syncTooltipWithDashboard = syncMode === DashboardCursorSync.Tooltip;
const syncTooltipWithDashboard = syncMode === 'TOOLTIP';
// Enable uPlot's built-in cursor sync when requested so that
// crosshair / tooltip can follow the dashboard-wide cursor.
if (syncMode !== DashboardCursorSync.None && config.scales[0]?.props.time) {
if (syncMode !== 'NONE' && config.scales[0]?.props.time) {
config.setCursor({
sync: { key: syncKey, scales: ['x', null] },
});

View File

@@ -4,6 +4,7 @@ import type {
ReactNode,
RefObject,
} from 'react';
import { CrossPanelSync } from 'types/api/dashboard/getAll';
import type uPlot from 'uplot';
import type { TooltipRenderArgs } from '../../components/types';
@@ -11,12 +12,6 @@ import type { UPlotConfigBuilder } from '../../config/UPlotConfigBuilder';
export const TOOLTIP_OFFSET = 10;
export enum DashboardCursorSync {
Crosshair,
None,
Tooltip,
}
export interface TooltipViewState {
plot?: uPlot | null;
style: Partial<CSSProperties>;
@@ -35,7 +30,7 @@ export interface TooltipLayoutInfo {
export interface TooltipPluginProps {
config: UPlotConfigBuilder;
canPinTooltip?: boolean;
syncMode?: DashboardCursorSync;
syncMode?: CrossPanelSync;
syncKey?: string;
render: (args: TooltipRenderArgs) => ReactNode;
maxWidth?: number;
@@ -86,7 +81,7 @@ export interface TooltipControllerContext {
rafId: MutableRefObject<number | null>;
updateState: (updates: Partial<TooltipViewState>) => void;
renderRef: MutableRefObject<(args: TooltipRenderArgs) => ReactNode>;
syncMode: DashboardCursorSync;
syncMode: CrossPanelSync;
syncKey: string;
canPinTooltip: boolean;
createTooltipContents: () => React.ReactNode;

View File

@@ -7,7 +7,6 @@ import type uPlot from 'uplot';
import { TooltipRenderArgs } from '../../components/types';
import { UPlotConfigBuilder } from '../../config/UPlotConfigBuilder';
import TooltipPlugin from '../TooltipPlugin/TooltipPlugin';
import { DashboardCursorSync } from '../TooltipPlugin/types';
// ---------------------------------------------------------------------------
// Mock helpers
@@ -100,7 +99,7 @@ describe('TooltipPlugin', () => {
React.createElement(TooltipPlugin, {
config,
render: renderFn,
syncMode: DashboardCursorSync.None,
syncMode: 'NONE',
...extraProps,
}),
);
@@ -127,7 +126,7 @@ describe('TooltipPlugin', () => {
React.createElement(TooltipPlugin, {
config,
render: () => React.createElement('div', null, 'tooltip-body'),
syncMode: DashboardCursorSync.None,
syncMode: 'NONE',
}),
);
@@ -141,7 +140,7 @@ describe('TooltipPlugin', () => {
React.createElement(TooltipPlugin, {
config,
render: () => null,
syncMode: DashboardCursorSync.None,
syncMode: 'NONE',
}),
);
@@ -217,7 +216,7 @@ describe('TooltipPlugin', () => {
{ type: 'button', onClick: args.dismiss },
'Dismiss',
),
syncMode: DashboardCursorSync.None,
syncMode: 'NONE',
canPinTooltip: true,
}),
);
@@ -261,7 +260,7 @@ describe('TooltipPlugin', () => {
React.createElement(TooltipPlugin, {
config: config,
render: () => React.createElement('div', null, 'tooltip-body'),
syncMode: DashboardCursorSync.None,
syncMode: 'NONE',
canPinTooltip: true,
}),
);
@@ -305,7 +304,7 @@ describe('TooltipPlugin', () => {
React.createElement(TooltipPlugin, {
config,
render: () => React.createElement('div', null, 'pinned content'),
syncMode: DashboardCursorSync.None,
syncMode: 'NONE',
canPinTooltip: true,
}),
);
@@ -348,7 +347,7 @@ describe('TooltipPlugin', () => {
React.createElement(TooltipPlugin, {
config,
render: () => React.createElement('div', null, 'pinned content'),
syncMode: DashboardCursorSync.None,
syncMode: 'NONE',
canPinTooltip: true,
}),
);
@@ -398,7 +397,7 @@ describe('TooltipPlugin', () => {
React.createElement(TooltipPlugin, {
config,
render: () => null,
syncMode: DashboardCursorSync.Tooltip,
syncMode: 'TOOLTIP',
syncKey: 'dashboard-sync',
}),
);
@@ -417,7 +416,7 @@ describe('TooltipPlugin', () => {
React.createElement(TooltipPlugin, {
config,
render: () => null,
syncMode: DashboardCursorSync.None,
syncMode: 'NONE',
}),
);
@@ -433,7 +432,7 @@ describe('TooltipPlugin', () => {
React.createElement(TooltipPlugin, {
config,
render: () => null,
syncMode: DashboardCursorSync.Tooltip,
syncMode: 'TOOLTIP',
}),
);
@@ -453,7 +452,7 @@ describe('TooltipPlugin', () => {
React.createElement(TooltipPlugin, {
config,
render: () => null,
syncMode: DashboardCursorSync.None,
syncMode: 'NONE',
}),
);

View File

@@ -81,6 +81,13 @@ export interface DashboardTemplate {
previewImage: string;
}
export const CROSS_PANEL_SYNC_OPTIONS = [
'NONE',
'CROSSHAIR',
'TOOLTIP',
] as const;
export type CrossPanelSync = typeof CROSS_PANEL_SYNC_OPTIONS[number];
export interface DashboardData {
// uuid?: string;
description?: string;
@@ -93,6 +100,7 @@ export interface DashboardData {
variables: Record<string, IDashboardVariable>;
version?: string;
image?: string;
crossPanelSync?: CrossPanelSync;
}
export interface WidgetRow {