Compare commits

..

4 Commits

Author SHA1 Message Date
primus-bot[bot]
6d0e60822c chore(release): bump to v0.122.0 (#11204)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-05-06 08:01:25 +00:00
Abhi kumar
acdaef6c2e chore: added reference to crosspanel sync docs (#11200)
* chore: added reference to crosspanel sync docs

* chore: minor changes
2026-05-06 05:44:03 +00:00
Abhi kumar
a7690bdaa2 fix: added fix for all series in tooltip sync mode (#11197)
* fix: added fix for all series in tooltip sync mode

* chore: minor cleanup
2026-05-06 04:24:33 +00:00
Nityananda Gohain
a7fde606ca fix: use ms in prepareFillZeroArgsWithStep (#11196)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-05-05 18:27:19 +00:00
11 changed files with 133 additions and 108 deletions

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.121.1
image: signoz/signoz:v0.122.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.121.1
image: signoz/signoz:v0.122.0
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.121.1}
image: signoz/signoz:${VERSION:-v0.122.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.121.1}
image: signoz/signoz:${VERSION:-v0.122.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -24,6 +24,43 @@
line-height: 20px;
}
.crossPanelSyncSectionHeader {
display: flex;
align-items: center;
gap: 6px;
align-self: flex-start;
}
.crossPanelSyncInfoIcon {
cursor: help;
color: var(--l3-foreground);
}
.crossPanelSyncTooltipContent {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 300px;
}
.crossPanelSyncTooltipTitle {
font-size: 14px;
}
.crossPanelSyncTooltipDescription {
font-size: 12px;
line-height: 1.5;
}
.crossPanelSyncTooltipDocLink {
display: flex;
align-items: center;
gap: 4px;
color: var(--primary-background);
font-size: 12px;
margin-top: 4px;
}
.crossPanelSyncRow {
display: flex;
flex-direction: row;

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Col, Input, Radio, Select, Space, Typography } from 'antd';
import { Col, Input, Radio, Select, Space, Tooltip, Typography } from 'antd';
import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddTags';
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
@@ -10,7 +10,7 @@ import {
SyncTooltipFilterMode,
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { isEqual } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { Check, ExternalLink, Info, X } from '@signozhq/icons';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import styles from './GeneralSettings.module.scss';
@@ -173,9 +173,36 @@ function GeneralDashboardSettings(): JSX.Element {
</Space>
</Col>
<Col className={`${styles.overviewSettings} ${styles.crossPanelSyncGroup}`}>
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
Cross-Panel Sync
</Typography.Text>
<div className={styles.crossPanelSyncSectionHeader}>
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
Cross-Panel Sync
</Typography.Text>
<Tooltip
title={
<div className={styles.crossPanelSyncTooltipContent}>
<strong className={styles.crossPanelSyncTooltipTitle}>
Cross-Panel Sync
</strong>
<span className={styles.crossPanelSyncTooltipDescription}>
Sync crosshair and tooltip across all the dashboard panels
</span>
<a
href="https://signoz.io/docs/dashboards/interactivity/#cross-panel-sync"
target="_blank"
rel="noopener noreferrer"
className={styles.crossPanelSyncTooltipDocLink}
>
Learn more
<ExternalLink size={12} />
</a>
</div>
}
placement="top"
mouseEnterDelay={0.5}
>
<Info size={14} className={styles.crossPanelSyncInfoIcon} />
</Tooltip>
</div>
<div className={styles.crossPanelSyncRow}>
<div className={styles.crossPanelSyncInfo}>
<Typography.Text className={styles.crossPanelSyncTitle}>

View File

@@ -24,10 +24,15 @@ export default function Tooltip({
);
const showHeader = showTooltipHeader || activeItem != null;
// With a single series the active item is fully represented in the header —
// hide the divider and list to avoid showing a duplicate row.
const showList = tooltipContent.length > 1;
const showDivider = showList && showHeader;
// A single row collapses into the header when it's the active item, but
// must stay in the list when there's no active item (e.g. sync-driven
// tooltips with no focused series) — otherwise the row would vanish.
const showList =
tooltipContent.length > 1 ||
(tooltipContent.length === 1 && activeItem == null);
// The divider separates the active row in the header from the list; with
// no active item it has nothing to separate.
const showDivider = showList && showHeader && activeItem != null;
return (
<div

View File

@@ -4,7 +4,6 @@ import cx from 'classnames';
import uPlot from 'uplot';
import logEvent from 'api/common/logEvent';
import { syncCursorRegistry } from './syncCursorRegistry';
import { createSyncDisplayHook } from './syncDisplayHook';
import {
createInitialControllerState,
@@ -13,7 +12,6 @@ import {
createSetSeriesHandler,
getPlot,
isScrollEventInPlot,
shouldShowTooltipForSync,
updatePlotVisibility,
updateWindowSize,
} from './tooltipController';
@@ -302,17 +300,49 @@ export default function TooltipPlugin({
}
};
// Returns false when the cursor is offscreen so the caller can bail
// without scheduling side effects.
function pinAtCursor(): boolean {
const plot = getPlot(controller);
if (!plot) {
return false;
// Handles all tooltip-pin keyboard interactions:
// Escape — always releases the tooltip when pinned (never steals Escape
// from other handlers since we do not call stopPropagation).
// pinKey — toggles: pins when hovering+unpinned, unpins when pinned.
const handleKeyDown = (event: KeyboardEvent): void => {
// Escape: release-only (never toggles on).
if (event.key === 'Escape') {
if (controller.pinned) {
logEvent(Events.TOOLTIP_UNPINNED, {
id: config.getId(),
});
dismissTooltip();
}
return;
}
if (event.key.toLowerCase() !== pinKey.toLowerCase()) {
return;
}
// Toggle off: P pressed while already pinned.
if (controller.pinned) {
logEvent(Events.TOOLTIP_UNPINNED, {
id: config.getId(),
});
dismissTooltip();
return;
}
// Toggle on: P pressed while hovering.
const plot = getPlot(controller);
if (
!plot ||
!controller.hoverActive ||
controller.focusedSeriesIndex == null
) {
return;
}
const cursorLeft = plot.cursor.left ?? -1;
const cursorTop = plot.cursor.top ?? -1;
if (cursorLeft < 0 || cursorTop < 0) {
return false;
return;
}
const plotRect = plot.over.getBoundingClientRect();
@@ -330,71 +360,8 @@ export default function TooltipPlugin({
id: config.getId(),
});
scheduleRender(true);
return true;
}
// Escape always releases (never pins); pinKey toggles. Unpin fans out
// naturally — every pinned panel's document listener sees the same key
// event and dismisses itself — so only pinning needs a broadcast.
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Escape') {
if (controller.pinned) {
logEvent(Events.TOOLTIP_UNPINNED, {
id: config.getId(),
});
dismissTooltip();
}
return;
}
if (event.key.toLowerCase() !== pinKey.toLowerCase()) {
return;
}
if (controller.pinned) {
logEvent(Events.TOOLTIP_UNPINNED, {
id: config.getId(),
});
dismissTooltip();
return;
}
if (!controller.hoverActive || controller.focusedSeriesIndex == null) {
return;
}
if (!pinAtCursor()) {
return;
}
// Deferred to a macrotask so the same P keydown finishes dispatching
// to every panel's listener first. A microtask is not enough — those
// run between listener invocations, and once a receiver's `pinned`
// flips, its own listener takes the unpin branch on the same event.
if (syncTooltipWithDashboard) {
setTimeout(() => {
syncCursorRegistry.broadcastPin(syncKey);
}, 0);
}
};
// Skips the focusedSeriesIndex guard so single-series or no-overlap
// receivers still pin at the synced timestamp.
const handleSyncPinBroadcast = (): void => {
if (controller.pinned) {
return;
}
if (!shouldShowTooltipForSync(controller, syncTooltipWithDashboard)) {
return;
}
pinAtCursor();
};
const unsubscribeSyncPin =
syncTooltipWithDashboard && canPinTooltip
? syncCursorRegistry.subscribePin(syncKey, handleSyncPinBroadcast)
: null;
// Forward overlay clicks to the consumer-provided onClick callback.
const handleOverClick = (event: MouseEvent): void => {
const plot = getPlot(controller);
@@ -486,7 +453,6 @@ export default function TooltipPlugin({
removeSetLegendHook();
removeSetCursorHook();
removeSyncDisplayHook?.();
unsubscribeSyncPin?.();
if (overClickHandler) {
const plot = getPlot(controller);
plot?.over.removeEventListener('click', overClickHandler);

View File

@@ -17,9 +17,6 @@ const activeSeriesMetricBySyncKey = new Map<
Record<string, string> | null
>();
type SyncPinListener = () => void;
const pinListenersBySyncKey = new Map<string, Set<SyncPinListener>>();
export const syncCursorRegistry = {
setMetadata(syncKey: string, metadata: TooltipSyncMetadata | undefined): void {
metadataBySyncKey.set(syncKey, metadata);
@@ -39,20 +36,4 @@ export const syncCursorRegistry = {
getActiveSeriesMetric(syncKey: string): Record<string, string> | null {
return activeSeriesMetricBySyncKey.get(syncKey) ?? null;
},
subscribePin(syncKey: string, listener: SyncPinListener): () => void {
let listeners = pinListenersBySyncKey.get(syncKey);
if (!listeners) {
listeners = new Set();
pinListenersBySyncKey.set(syncKey, listeners);
}
listeners.add(listener);
return (): void => {
pinListenersBySyncKey.get(syncKey)?.delete(listener);
};
},
broadcastPin(syncKey: string): void {
pinListenersBySyncKey.get(syncKey)?.forEach((listener) => listener());
},
};

View File

@@ -137,7 +137,7 @@ function applyReceiverSync({
if (commonKeys.length === 0) {
uPlotInstance.setSeries(null, { focus: false });
return [];
return noMatchResult;
}
if ((uPlotInstance.cursor.left ?? -1) < 0) {

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/govaluate"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/querybuilder"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
@@ -969,11 +970,19 @@ func (q *querier) prepareFillZeroArgsWithStep(functions []qbtypes.Function, req
updatedFunctions := make([]qbtypes.Function, len(functions))
copy(updatedFunctions, functions)
// funcFillZero expects start/end in milliseconds. req.Start/req.End may
// arrive in s/ms/μs/ns depending on the caller; normalize via ToNanoSecs
// (same pattern used elsewhere in the codebase, e.g. RecommendedStepInterval)
// then convert to ms. Without this, an ns payload makes (end-start)/step
// 10^6× too large and OOMs the process.
startMs := querybuilder.ToNanoSecs(req.Start) / 1_000_000
endMs := querybuilder.ToNanoSecs(req.End) / 1_000_000
for i, fn := range updatedFunctions {
if fn.Name == qbtypes.FunctionNameFillZero && len(fn.Args) == 0 {
fn.Args = []qbtypes.FunctionArg{
{Value: float64(req.Start)},
{Value: float64(req.End)},
{Value: float64(startMs)},
{Value: float64(endMs)},
{Value: float64(step)},
}
updatedFunctions[i] = fn