mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-16 08:50:29 +01:00
Compare commits
2 Commits
chore/user
...
chore/tool
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e1916daa6 | ||
|
|
7eb8806c0f |
@@ -4,5 +4,4 @@ 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',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import ChartLayout from 'container/DashboardContainer/visualization/layout/ChartLayout/ChartLayout';
|
||||
import Legend from 'lib/uPlotV2/components/Legend/Legend';
|
||||
import {
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
|
||||
@@ -27,6 +28,7 @@ export default function ChartWrapper({
|
||||
canPinTooltip = false,
|
||||
syncMode,
|
||||
syncKey,
|
||||
yAxisUnit,
|
||||
onDestroy = noop,
|
||||
children,
|
||||
layoutChildren,
|
||||
@@ -34,6 +36,9 @@ 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(
|
||||
@@ -99,6 +104,7 @@ export default function ChartWrapper({
|
||||
averageLegendWidth + TOOLTIP_WIDTH_PADDING,
|
||||
)}
|
||||
syncKey={syncKey}
|
||||
panelMetadata={panelMetadata}
|
||||
render={renderTooltipCallback}
|
||||
pinnedTooltipElement={pinnedTooltipElement}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import cx from 'classnames';
|
||||
import { getFocusedSeriesAtPosition } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { syncCursorRegistry } from './syncCursorRegistry';
|
||||
import {
|
||||
createInitialControllerState,
|
||||
createSetCursorHandler,
|
||||
@@ -40,6 +41,7 @@ export default function TooltipPlugin({
|
||||
maxHeight = 600,
|
||||
syncMode = DashboardCursorSync.None,
|
||||
syncKey = '_tooltip_sync_global_',
|
||||
panelMetadata,
|
||||
pinnedTooltipElement,
|
||||
canPinTooltip = false,
|
||||
}: TooltipPluginProps): JSX.Element | null {
|
||||
@@ -100,7 +102,29 @@ 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', null] },
|
||||
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';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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);
|
||||
},
|
||||
};
|
||||
@@ -4,11 +4,29 @@ 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 {
|
||||
@@ -39,6 +57,9 @@ 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;
|
||||
|
||||
@@ -516,7 +516,7 @@ describe('TooltipPlugin', () => {
|
||||
);
|
||||
|
||||
expect(setCursorSpy).toHaveBeenCalledWith({
|
||||
sync: { key: 'dashboard-sync', scales: ['x', null] },
|
||||
sync: { key: 'dashboard-sync', scales: ['x', 'y'] },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -21,7 +21,6 @@ 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 }
|
||||
@@ -42,7 +41,6 @@ func NewName(name string) (Name, error) {
|
||||
NameLastSeenChangelogVersion.StringValue(),
|
||||
NameSpanDetailsPinnedAttributes.StringValue(),
|
||||
NameSpanPercentileResourceAttributes.StringValue(),
|
||||
NameDashboardPreferences.StringValue(),
|
||||
},
|
||||
name,
|
||||
)
|
||||
|
||||
@@ -172,15 +172,6 @@ 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user