Compare commits

..

1 Commits

Author SHA1 Message Date
Abhi Kumar
a4843b7831 chore: added backend changes for storing user dashbaord preference 2026-04-16 12:54:14 +05:30
9 changed files with 95 additions and 84 deletions

View File

@@ -4,4 +4,5 @@ export const USER_PREFERENCES = {
LAST_SEEN_CHANGELOG_VERSION: 'last_seen_changelog_version',
SPAN_DETAILS_PINNED_ATTRIBUTES: 'span_details_pinned_attributes',
SPAN_PERCENTILE_RESOURCE_ATTRIBUTES: 'span_percentile_resource_attributes',
DASHBOARD_PREFERENCES: 'dashboard_preferences',
};

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useRef } from 'react';
import ChartLayout from 'container/DashboardContainer/visualization/layout/ChartLayout/ChartLayout';
import Legend from 'lib/uPlotV2/components/Legend/Legend';
import {
@@ -8,7 +8,6 @@ import {
import UPlotChart from 'lib/uPlotV2/components/UPlotChart/UPlotChart';
import { PlotContextProvider } from 'lib/uPlotV2/context/PlotContext';
import TooltipPlugin from 'lib/uPlotV2/plugins/TooltipPlugin/TooltipPlugin';
import { CursorSyncPanelMetadata } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import noop from 'lodash-es/noop';
import uPlot from 'uplot';
@@ -28,7 +27,6 @@ export default function ChartWrapper({
canPinTooltip = false,
syncMode,
syncKey,
yAxisUnit,
onDestroy = noop,
children,
layoutChildren,
@@ -36,9 +34,6 @@ export default function ChartWrapper({
pinnedTooltipElement,
'data-testid': testId,
}: ChartProps): JSX.Element {
const panelMetadata = useMemo<CursorSyncPanelMetadata>(() => ({ yAxisUnit }), [
yAxisUnit,
]);
const plotInstanceRef = useRef<uPlot | null>(null);
const legendComponent = useCallback(
@@ -104,7 +99,6 @@ export default function ChartWrapper({
averageLegendWidth + TOOLTIP_WIDTH_PADDING,
)}
syncKey={syncKey}
panelMetadata={panelMetadata}
render={renderTooltipCallback}
pinnedTooltipElement={pinnedTooltipElement}
/>

View File

@@ -4,7 +4,6 @@ import cx from 'classnames';
import { getFocusedSeriesAtPosition } from 'lib/uPlotLib/plugins/onClickPlugin';
import uPlot from 'uplot';
import { syncCursorRegistry } from './syncCursorRegistry';
import {
createInitialControllerState,
createSetCursorHandler,
@@ -41,7 +40,6 @@ export default function TooltipPlugin({
maxHeight = 600,
syncMode = DashboardCursorSync.None,
syncKey = '_tooltip_sync_global_',
panelMetadata,
pinnedTooltipElement,
canPinTooltip = false,
}: TooltipPluginProps): JSX.Element | null {
@@ -102,29 +100,7 @@ export default function TooltipPlugin({
// crosshair / tooltip can follow the dashboard-wide cursor.
if (syncMode !== DashboardCursorSync.None && config.scales[0]?.props.time) {
config.setCursor({
sync: { key: syncKey, scales: ['x', 'y'] },
});
// Show the horizontal crosshair only when the receiving panel shares
// the same y-axis unit as the source panel. When this panel is the
// source (cursor.event != null) the line is always shown and this
// panel's metadata is written to the registry so receivers can read it.
config.addHook('setCursor', (u: uPlot): void => {
const yCursorEl = u.root.querySelector<HTMLElement>('.u-cursor-y');
if (!yCursorEl) {
return;
}
if (u.cursor.event != null) {
// This panel is the source — publish metadata and always show line.
syncCursorRegistry.setMetadata(syncKey, panelMetadata);
yCursorEl.style.display = '';
} else {
// This panel is receiving sync — show only if units match.
const sourceMeta = syncCursorRegistry.getMetadata(syncKey);
yCursorEl.style.display =
sourceMeta?.yAxisUnit === panelMetadata?.yAxisUnit ? '' : 'none';
}
sync: { key: syncKey, scales: ['x', null] },
});
}

View File

@@ -1,30 +0,0 @@
import type { CursorSyncPanelMetadata } from './types';
/**
* Module-level registry that tracks the metadata of the panel currently
* acting as the cursor source (the one being hovered) per sync group.
*
* uPlot fires the source panel's setCursor hook before broadcasting to
* receivers, so the registry is always populated before receivers read it.
*
* Receivers use this to make decisions such as:
* - Whether to show the horizontal crosshair line (matching yAxisUnit)
* - Future: what to render inside the tooltip (matching groupBy, etc.)
*/
const metadataBySyncKey = new Map<
string,
CursorSyncPanelMetadata | undefined
>();
export const syncCursorRegistry = {
setMetadata(
syncKey: string,
metadata: CursorSyncPanelMetadata | undefined,
): void {
metadataBySyncKey.set(syncKey, metadata);
},
getMetadata(syncKey: string): CursorSyncPanelMetadata | undefined {
return metadataBySyncKey.get(syncKey);
},
};

View File

@@ -4,29 +4,11 @@ import type {
ReactNode,
RefObject,
} from 'react';
import type { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type uPlot from 'uplot';
import type { TooltipRenderArgs } from '../../components/types';
import type { UPlotConfigBuilder } from '../../config/UPlotConfigBuilder';
/**
* Per-panel metadata shared with the cursor sync registry so that
* panels receiving a synced cursor can make informed decisions about
* rendering (e.g. whether to show the horizontal crosshair, what to
* show in the tooltip for the source panel's context).
*
* Add new fields here as sync-aware features need them.
*/
export interface CursorSyncPanelMetadata {
/** Y-axis unit of this panel (e.g. 'ms', 'req/s'). Used to decide
* whether the horizontal crosshair should be shown on receiving panels. */
yAxisUnit?: string;
/** Label dimensions this panel groups by. Will be used to enrich
* tooltip content on receiving panels to show correlated series. */
groupBy?: BaseAutocompleteData[];
}
export const TOOLTIP_OFFSET = 10;
export enum DashboardCursorSync {
@@ -57,9 +39,6 @@ export interface TooltipPluginProps {
canPinTooltip?: boolean;
syncMode?: DashboardCursorSync;
syncKey?: string;
/** Metadata about this panel shared with the sync registry so that
* receiving panels can make context-aware rendering decisions. */
panelMetadata?: CursorSyncPanelMetadata;
render: (args: TooltipRenderArgs) => ReactNode;
pinnedTooltipElement?: (clickData: TooltipClickData) => ReactNode;
maxWidth?: number;

View File

@@ -516,7 +516,7 @@ describe('TooltipPlugin', () => {
);
expect(setCursorSpy).toHaveBeenCalledWith({
sync: { key: 'dashboard-sync', scales: ['x', 'y'] },
sync: { key: 'dashboard-sync', scales: ['x', null] },
});
});

View File

@@ -0,0 +1,80 @@
package preferencetypes
import (
"encoding/json"
"slices"
"github.com/SigNoz/signoz/pkg/errors"
)
// CursorSyncMode controls how chart cursors are synchronised across panels in a dashboard.
type CursorSyncMode string
const (
CursorSyncModeCrosshair CursorSyncMode = "crosshair"
CursorSyncModeTooltip CursorSyncMode = "tooltip"
CursorSyncModeNone CursorSyncMode = "none"
)
var allowedCursorSyncModes = []CursorSyncMode{
CursorSyncModeCrosshair,
CursorSyncModeTooltip,
CursorSyncModeNone,
}
// DashboardPreference holds user-specific overrides for a single dashboard.
type DashboardPreference struct {
CursorSyncMode CursorSyncMode `json:"cursorSyncMode"`
}
func (p DashboardPreference) Validate() error {
if !slices.Contains(allowedCursorSyncModes, p.CursorSyncMode) {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput,
"invalid cursorSyncMode %q: must be one of crosshair, tooltip, none", p.CursorSyncMode)
}
return nil
}
// DashboardPreferences maps dashboard IDs to their per-dashboard user overrides.
// The key is the dashboard UUID string.
type DashboardPreferences map[string]DashboardPreference
func (p DashboardPreferences) Validate() error {
for id, pref := range p {
if err := pref.Validate(); err != nil {
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput,
"invalid preference for dashboard %s: %s", id, err.Error())
}
}
return nil
}
// NewDashboardPreferencesValue validates prefs and wraps it in a Value suitable
// for storage as the dashboard_preferences user preference.
func NewDashboardPreferencesValue(prefs DashboardPreferences) (Value, error) {
if err := prefs.Validate(); err != nil {
return Value{}, err
}
// DashboardPreferences is map[string]DashboardPreference — a map kind — so
// NewValue's ValueTypeObject check passes without any reflection gymnastics.
return NewValue(prefs, ValueTypeObject)
}
// DashboardPreferencesFromValue decodes a Value that was stored as
// dashboard_preferences back into the strongly-typed DashboardPreferences map.
func DashboardPreferencesFromValue(v Value) (DashboardPreferences, error) {
// MarshalJSON returns the raw JSON string stored inside Value.
jsonBytes, err := json.Marshal(v)
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput,
"cannot marshal dashboard preferences value")
}
var prefs DashboardPreferences
if err := json.Unmarshal(jsonBytes, &prefs); err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput,
"cannot decode dashboard preferences")
}
return prefs, nil
}

View File

@@ -21,6 +21,7 @@ var (
NameLastSeenChangelogVersion = Name{valuer.NewString("last_seen_changelog_version")}
NameSpanDetailsPinnedAttributes = Name{valuer.NewString("span_details_pinned_attributes")}
NameSpanPercentileResourceAttributes = Name{valuer.NewString("span_percentile_resource_attributes")}
NameDashboardPreferences = Name{valuer.NewString("dashboard_preferences")}
)
type Name struct{ valuer.String }
@@ -41,6 +42,7 @@ func NewName(name string) (Name, error) {
NameLastSeenChangelogVersion.StringValue(),
NameSpanDetailsPinnedAttributes.StringValue(),
NameSpanPercentileResourceAttributes.StringValue(),
NameDashboardPreferences.StringValue(),
},
name,
)

View File

@@ -172,6 +172,15 @@ func NewAvailablePreference() map[Name]Preference {
AllowedValues: []string{},
Value: MustNewValue([]any{}, ValueTypeArray),
},
NameDashboardPreferences: {
Name: NameDashboardPreferences,
Description: "User preferences for dashboards, such as cursor sync behaviour. Keyed by dashboard ID.",
ValueType: ValueTypeObject,
DefaultValue: MustNewValue(DashboardPreferences{}, ValueTypeObject),
AllowedScopes: []Scope{ScopeUser},
AllowedValues: []string{},
Value: MustNewValue(DashboardPreferences{}, ValueTypeObject),
},
}
}