mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-06 10:30:31 +01:00
Compare commits
1 Commits
main
...
fix/multi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07d9a9ecb6 |
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.122.0
|
||||
image: signoz/signoz:v0.121.1
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.122.0
|
||||
image: signoz/signoz:v0.121.1
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -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.122.0}
|
||||
image: signoz/signoz:${VERSION:-v0.121.1}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -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.122.0}
|
||||
image: signoz/signoz:${VERSION:-v0.121.1}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -24,43 +24,6 @@
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Col, Input, Radio, Select, Space, Tooltip, Typography } from 'antd';
|
||||
import { Col, Input, Radio, Select, Space, 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, ExternalLink, Info, X } from '@signozhq/icons';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import styles from './GeneralSettings.module.scss';
|
||||
@@ -173,36 +173,9 @@ function GeneralDashboardSettings(): JSX.Element {
|
||||
</Space>
|
||||
</Col>
|
||||
<Col className={`${styles.overviewSettings} ${styles.crossPanelSyncGroup}`}>
|
||||
<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>
|
||||
<Typography.Text className={styles.crossPanelSyncSectionTitle}>
|
||||
Cross-Panel Sync
|
||||
</Typography.Text>
|
||||
<div className={styles.crossPanelSyncRow}>
|
||||
<div className={styles.crossPanelSyncInfo}>
|
||||
<Typography.Text className={styles.crossPanelSyncTitle}>
|
||||
|
||||
@@ -24,15 +24,10 @@ export default function Tooltip({
|
||||
);
|
||||
|
||||
const showHeader = showTooltipHeader || activeItem != null;
|
||||
// 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;
|
||||
// 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;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -4,6 +4,7 @@ import cx from 'classnames';
|
||||
import uPlot from 'uplot';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
|
||||
import { syncCursorRegistry } from './syncCursorRegistry';
|
||||
import { createSyncDisplayHook } from './syncDisplayHook';
|
||||
import {
|
||||
createInitialControllerState,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
createSetSeriesHandler,
|
||||
getPlot,
|
||||
isScrollEventInPlot,
|
||||
shouldShowTooltipForSync,
|
||||
updatePlotVisibility,
|
||||
updateWindowSize,
|
||||
} from './tooltipController';
|
||||
@@ -300,49 +302,17 @@ export default function TooltipPlugin({
|
||||
}
|
||||
};
|
||||
|
||||
// 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.
|
||||
// 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 ||
|
||||
!controller.hoverActive ||
|
||||
controller.focusedSeriesIndex == null
|
||||
) {
|
||||
return;
|
||||
if (!plot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cursorLeft = plot.cursor.left ?? -1;
|
||||
const cursorTop = plot.cursor.top ?? -1;
|
||||
if (cursorLeft < 0 || cursorTop < 0) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const plotRect = plot.over.getBoundingClientRect();
|
||||
@@ -360,8 +330,71 @@ 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);
|
||||
@@ -453,6 +486,7 @@ export default function TooltipPlugin({
|
||||
removeSetLegendHook();
|
||||
removeSetCursorHook();
|
||||
removeSyncDisplayHook?.();
|
||||
unsubscribeSyncPin?.();
|
||||
if (overClickHandler) {
|
||||
const plot = getPlot(controller);
|
||||
plot?.over.removeEventListener('click', overClickHandler);
|
||||
|
||||
@@ -17,6 +17,9 @@ 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);
|
||||
@@ -36,4 +39,20 @@ 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());
|
||||
},
|
||||
};
|
||||
|
||||
@@ -137,7 +137,7 @@ function applyReceiverSync({
|
||||
|
||||
if (commonKeys.length === 0) {
|
||||
uPlotInstance.setSeries(null, { focus: false });
|
||||
return noMatchResult;
|
||||
return [];
|
||||
}
|
||||
|
||||
if ((uPlotInstance.cursor.left ?? -1) < 0) {
|
||||
|
||||
@@ -12,7 +12,6 @@ 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"
|
||||
)
|
||||
@@ -970,19 +969,11 @@ 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(startMs)},
|
||||
{Value: float64(endMs)},
|
||||
{Value: float64(req.Start)},
|
||||
{Value: float64(req.End)},
|
||||
{Value: float64(step)},
|
||||
}
|
||||
updatedFunctions[i] = fn
|
||||
|
||||
Reference in New Issue
Block a user