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 { .overview-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px;
.overview-settings { .overview-settings {
border-radius: 3px; border-radius: 3px;
@@ -55,6 +56,35 @@
border: 1px solid var(--bg-slate-400); border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300); 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 { .overview-settings-footer {
@@ -168,6 +198,15 @@
border: 1px solid var(--bg-vanilla-300); border: 1px solid var(--bg-vanilla-300);
background: 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 { .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 { 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 AddTags from 'container/DashboardContainer/DashboardSettings/General/AddTags';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard'; import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
CROSS_PANEL_SYNC_OPTIONS,
CrossPanelSync,
} from 'types/api/dashboard/getAll';
import { Button } from './styles'; import { Button } from './styles';
import { Base64Icons } from './utils'; import { Base64Icons } from './utils';
@@ -21,8 +25,13 @@ function GeneralDashboardSettings(): JSX.Element {
const selectedData = selectedDashboard?.data; const selectedData = selectedDashboard?.data;
const { title = '', tags = [], description = '', image = Base64Icons[0] } = const {
selectedData || {}; title = '',
tags = [],
description = '',
image = Base64Icons[0],
crossPanelSync = 'NONE',
} = selectedData || {};
const [updatedTitle, setUpdatedTitle] = useState<string>(title); const [updatedTitle, setUpdatedTitle] = useState<string>(title);
const [updatedTags, setUpdatedTags] = useState<string[]>(tags || []); const [updatedTags, setUpdatedTags] = useState<string[]>(tags || []);
@@ -30,6 +39,10 @@ function GeneralDashboardSettings(): JSX.Element {
description || '', description || '',
); );
const [updatedImage, setUpdatedImage] = useState<string>(image); const [updatedImage, setUpdatedImage] = useState<string>(image);
const [
updatedCrossPanelSync,
setUpdatedCrossPanelSync,
] = useState<CrossPanelSync>(crossPanelSync);
const [numberOfUnsavedChanges, setNumberOfUnsavedChanges] = useState<number>( const [numberOfUnsavedChanges, setNumberOfUnsavedChanges] = useState<number>(
0, 0,
); );
@@ -50,6 +63,7 @@ function GeneralDashboardSettings(): JSX.Element {
tags: updatedTags, tags: updatedTags,
title: updatedTitle, title: updatedTitle,
image: updatedImage, image: updatedImage,
crossPanelSync: updatedCrossPanelSync,
}, },
}, },
{ {
@@ -65,12 +79,13 @@ function GeneralDashboardSettings(): JSX.Element {
useEffect(() => { useEffect(() => {
let numberOfUnsavedChanges = 0; let numberOfUnsavedChanges = 0;
const initialValues = [title, description, tags, image]; const initialValues = [title, description, tags, image, crossPanelSync];
const updatedValues = [ const updatedValues = [
updatedTitle, updatedTitle,
updatedDescription, updatedDescription,
updatedTags, updatedTags,
updatedImage, updatedImage,
updatedCrossPanelSync,
]; ];
initialValues.forEach((val, index) => { initialValues.forEach((val, index) => {
if (!isEqual(val, updatedValues[index])) { if (!isEqual(val, updatedValues[index])) {
@@ -79,21 +94,38 @@ function GeneralDashboardSettings(): JSX.Element {
}); });
setNumberOfUnsavedChanges(numberOfUnsavedChanges); setNumberOfUnsavedChanges(numberOfUnsavedChanges);
}, [ }, [
crossPanelSync,
description, description,
image, image,
tags, tags,
title, title,
updatedCrossPanelSync,
updatedDescription, updatedDescription,
updatedImage, updatedImage,
updatedTags, updatedTags,
updatedTitle, 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 => { const discardHandler = (): void => {
setUpdatedTitle(title); setUpdatedTitle(title);
setUpdatedImage(image); setUpdatedImage(image);
setUpdatedTags(tags); setUpdatedTags(tags);
setUpdatedDescription(description); setUpdatedDescription(description);
setUpdatedCrossPanelSync(crossPanelSync);
}; };
return ( return (
@@ -156,6 +188,28 @@ function GeneralDashboardSettings(): JSX.Element {
</div> </div>
</Space> </Space>
</Col> </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 && ( {numberOfUnsavedChanges > 0 && (
<div className="overview-settings-footer"> <div className="overview-settings-footer">
<div className="unsaved"> <div className="unsaved">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,6 +81,13 @@ export interface DashboardTemplate {
previewImage: string; 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 { export interface DashboardData {
// uuid?: string; // uuid?: string;
description?: string; description?: string;
@@ -93,6 +100,7 @@ export interface DashboardData {
variables: Record<string, IDashboardVariable>; variables: Record<string, IDashboardVariable>;
version?: string; version?: string;
image?: string; image?: string;
crossPanelSync?: CrossPanelSync;
} }
export interface WidgetRow { export interface WidgetRow {