Compare commits

..

5 Commits

Author SHA1 Message Date
Abhi kumar
e85395acbf Merge branch 'main' into feat/cross-panel-sync 2026-02-16 20:56:31 +05:30
SagarRajput-7
aa39db8ac2 feat: updated onboarding question and used radio from signozhq (#10317)
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
* feat: updated onboarding question and used radio from signozhq

* feat: added and updated test cases

* feat: removed the editorg and updateorg from onboarding questionnaire

* feat: addressed comments and feedback
2026-02-16 20:55:18 +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
27 changed files with 444 additions and 1006 deletions

View File

@@ -55,6 +55,7 @@
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/radio-group": "0.0.2",
"@signozhq/resizable": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/table": "0.3.7",

View File

@@ -21,6 +21,7 @@ import '@signozhq/design-tokens';
import '@signozhq/icons';
import '@signozhq/input';
import '@signozhq/popover';
import '@signozhq/radio-group';
import '@signozhq/resizable';
import '@signozhq/sonner';
import '@signozhq/table';

View File

@@ -1,6 +1,7 @@
.overview-content {
display: flex;
flex-direction: column;
gap: 16px;
.overview-settings {
border-radius: 3px;
@@ -55,6 +56,35 @@
border: 1px solid var(--bg-slate-400);
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 {
@@ -168,6 +198,15 @@
border: 1px solid 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 {

View File

@@ -1,11 +1,15 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
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 { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { isEqual } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
CROSS_PANEL_SYNC_OPTIONS,
CrossPanelSync,
} from 'types/api/dashboard/getAll';
import { Button } from './styles';
import { Base64Icons } from './utils';
@@ -21,8 +25,13 @@ function GeneralDashboardSettings(): JSX.Element {
const selectedData = selectedDashboard?.data;
const { title = '', tags = [], description = '', image = Base64Icons[0] } =
selectedData || {};
const {
title = '',
tags = [],
description = '',
image = Base64Icons[0],
crossPanelSync = 'NONE',
} = selectedData || {};
const [updatedTitle, setUpdatedTitle] = useState<string>(title);
const [updatedTags, setUpdatedTags] = useState<string[]>(tags || []);
@@ -30,6 +39,10 @@ function GeneralDashboardSettings(): JSX.Element {
description || '',
);
const [updatedImage, setUpdatedImage] = useState<string>(image);
const [
updatedCrossPanelSync,
setUpdatedCrossPanelSync,
] = useState<CrossPanelSync>(crossPanelSync);
const [numberOfUnsavedChanges, setNumberOfUnsavedChanges] = useState<number>(
0,
);
@@ -50,6 +63,7 @@ function GeneralDashboardSettings(): JSX.Element {
tags: updatedTags,
title: updatedTitle,
image: updatedImage,
crossPanelSync: updatedCrossPanelSync,
},
},
{
@@ -65,12 +79,13 @@ function GeneralDashboardSettings(): JSX.Element {
useEffect(() => {
let numberOfUnsavedChanges = 0;
const initialValues = [title, description, tags, image];
const initialValues = [title, description, tags, image, crossPanelSync];
const updatedValues = [
updatedTitle,
updatedDescription,
updatedTags,
updatedImage,
updatedCrossPanelSync,
];
initialValues.forEach((val, index) => {
if (!isEqual(val, updatedValues[index])) {
@@ -79,21 +94,38 @@ function GeneralDashboardSettings(): JSX.Element {
});
setNumberOfUnsavedChanges(numberOfUnsavedChanges);
}, [
crossPanelSync,
description,
image,
tags,
title,
updatedCrossPanelSync,
updatedDescription,
updatedImage,
updatedTags,
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 => {
setUpdatedTitle(title);
setUpdatedImage(image);
setUpdatedTags(tags);
setUpdatedDescription(description);
setUpdatedCrossPanelSync(crossPanelSync);
};
return (
@@ -156,6 +188,28 @@ function GeneralDashboardSettings(): JSX.Element {
</div>
</Space>
</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 && (
<div className="overview-settings-footer">
<div className="unsaved">

View File

@@ -23,7 +23,6 @@ export default function ChartWrapper({
width: containerWidth,
height: containerHeight,
showTooltip = true,
showLegend = true,
canPinTooltip = false,
syncMode,
syncKey,
@@ -37,9 +36,6 @@ export default function ChartWrapper({
const legendComponent = useCallback(
(averageLegendWidth: number): React.ReactNode => {
if (!showLegend) {
return null;
}
return (
<Legend
config={config}
@@ -48,7 +44,7 @@ export default function ChartWrapper({
/>
);
},
[config, legendConfig.position, showLegend],
[config, legendConfig.position],
);
const renderTooltipCallback = useCallback(
@@ -64,7 +60,6 @@ export default function ChartWrapper({
return (
<PlotContextProvider>
<ChartLayout
showLegend={showLegend}
config={config}
containerWidth={containerWidth}
containerHeight={containerHeight}

View File

@@ -1,55 +0,0 @@
import { useCallback } from 'react';
import ChartWrapper from 'container/DashboardContainer/visualization/charts/ChartWrapper/ChartWrapper';
import HistogramTooltip from 'lib/uPlotV2/components/Tooltip/HistogramTooltip';
import { buildTooltipContent } from 'lib/uPlotV2/components/Tooltip/utils';
import {
HistogramTooltipProps,
TooltipRenderArgs,
} from 'lib/uPlotV2/components/types';
import { HistogramChartProps } from '../types';
export default function Histogram(props: HistogramChartProps): JSX.Element {
const {
children,
renderTooltip: customRenderTooltip,
isQueriesMerged,
...rest
} = props;
const renderTooltip = useCallback(
(props: TooltipRenderArgs): React.ReactNode => {
if (customRenderTooltip) {
return customRenderTooltip(props);
}
const content = buildTooltipContent({
data: props.uPlotInstance.data,
series: props.uPlotInstance.series,
dataIndexes: props.dataIndexes,
activeSeriesIndex: props.seriesIndex,
uPlotInstance: props.uPlotInstance,
yAxisUnit: rest.yAxisUnit ?? '',
decimalPrecision: rest.decimalPrecision,
});
const tooltipProps: HistogramTooltipProps = {
...props,
timezone: rest.timezone,
yAxisUnit: rest.yAxisUnit,
decimalPrecision: rest.decimalPrecision,
content,
};
return <HistogramTooltip {...tooltipProps} />;
},
[customRenderTooltip, rest.timezone, rest.yAxisUnit, rest.decimalPrecision],
);
return (
<ChartWrapper
showLegend={!isQueriesMerged}
{...rest}
renderTooltip={renderTooltip}
>
{children}
</ChartWrapper>
);
}

View File

@@ -1,13 +1,12 @@
import { PrecisionOption } from 'components/Graph/types';
import { LegendConfig, TooltipRenderArgs } from 'lib/uPlotV2/components/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { CrossPanelSync } from 'types/api/dashboard/getAll';
interface BaseChartProps {
width: number;
height: number;
showTooltip?: boolean;
showLegend?: boolean;
timezone: string;
canPinTooltip?: boolean;
yAxisUnit?: string;
@@ -18,8 +17,7 @@ interface BaseChartProps {
interface UPlotBasedChartProps {
config: UPlotConfigBuilder;
data: uPlot.AlignedData;
legendConfig: LegendConfig;
syncMode?: DashboardCursorSync;
syncMode?: CrossPanelSync;
syncKey?: string;
plotRef?: (plot: uPlot | null) => void;
onDestroy?: (plot: uPlot) => void;
@@ -28,20 +26,14 @@ interface UPlotBasedChartProps {
}
export interface TimeSeriesChartProps
extends BaseChartProps,
UPlotBasedChartProps {}
export interface HistogramChartProps
extends BaseChartProps,
UPlotBasedChartProps {
isQueriesMerged?: boolean;
legendConfig: LegendConfig;
}
export interface BarChartProps extends BaseChartProps, UPlotBasedChartProps {
legendConfig: LegendConfig;
isStackedBarChart?: boolean;
}
export type ChartProps =
| TimeSeriesChartProps
| BarChartProps
| HistogramChartProps;
export type ChartProps = TimeSeriesChartProps | BarChartProps;

View File

@@ -1,14 +1,12 @@
import { useMemo } from 'react';
import cx from 'classnames';
import { calculateChartDimensions } from 'container/DashboardContainer/visualization/charts/utils';
import { MAX_LEGEND_WIDTH } from 'lib/uPlotV2/components/Legend/Legend';
import { LegendConfig, LegendPosition } from 'lib/uPlotV2/components/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import './ChartLayout.styles.scss';
export interface ChartLayoutProps {
showLegend?: boolean;
legendComponent: (legendPerSet: number) => React.ReactNode;
children: (props: {
chartWidth: number;
@@ -22,7 +20,6 @@ export interface ChartLayoutProps {
config: UPlotConfigBuilder;
}
export default function ChartLayout({
showLegend = true,
legendComponent,
children,
layoutChildren,
@@ -33,15 +30,6 @@ export default function ChartLayout({
}: ChartLayoutProps): JSX.Element {
const chartDimensions = useMemo(
() => {
if (!showLegend) {
return {
width: containerWidth,
height: containerHeight,
legendWidth: 0,
legendHeight: 0,
averageLegendWidth: MAX_LEGEND_WIDTH,
};
}
const legendItemsMap = config.getLegendItems();
const seriesLabels = Object.values(legendItemsMap)
.map((item) => item.label)
@@ -54,7 +42,7 @@ export default function ChartLayout({
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[containerWidth, containerHeight, legendConfig, showLegend],
[containerWidth, containerHeight, legendConfig],
);
return (
@@ -72,17 +60,15 @@ export default function ChartLayout({
averageLegendWidth: chartDimensions.averageLegendWidth,
})}
</div>
{showLegend && (
<div
className="chart-layout__legend-wrapper"
style={{
height: chartDimensions.legendHeight,
width: chartDimensions.legendWidth,
}}
>
{legendComponent(chartDimensions.averageLegendWidth)}
</div>
)}
<div
className="chart-layout__legend-wrapper"
style={{
height: chartDimensions.legendHeight,
width: chartDimensions.legendWidth,
}}
>
{legendComponent(chartDimensions.averageLegendWidth)}
</div>
</div>
{layoutChildren}
</div>

View File

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

View File

@@ -1,121 +0,0 @@
import { useEffect, useMemo, useRef } from 'react';
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import Histogram from '../../charts/Histogram/Histogram';
import ChartManager from '../../components/ChartManager/ChartManager';
import {
prepareHistogramPanelConfig,
prepareHistogramPanelData,
} from './utils';
function HistogramPanel(props: PanelWrapperProps): JSX.Element {
const {
panelMode,
queryResponse,
widget,
isFullViewMode,
onToggleModelHandler,
} = props;
const uPlotRef = useRef<uPlot | null>(null);
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const graphRef = useRef<HTMLDivElement>(null);
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
useEffect(() => {
if (toScrollWidgetId === widget.id) {
graphRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
graphRef.current?.focus();
setToScrollWidgetId('');
}
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
const config = useMemo(() => {
return prepareHistogramPanelConfig({
widget,
isDarkMode,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
panelMode,
});
}, [widget, isDarkMode, queryResponse?.data?.payload, panelMode]);
const chartData = useMemo(() => {
if (!queryResponse?.data?.payload) {
return [];
}
return prepareHistogramPanelData({
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
bucketWidth: widget?.bucketWidth,
bucketCount: widget?.bucketCount,
mergeAllActiveQueries: widget?.mergeAllActiveQueries,
});
}, [
queryResponse?.data?.payload,
widget?.bucketWidth,
widget?.bucketCount,
widget?.mergeAllActiveQueries,
]);
const layoutChildren = useMemo(() => {
if (!isFullViewMode) {
return null;
}
return (
<ChartManager
config={config}
alignedData={chartData}
yAxisUnit={widget.yAxisUnit}
onCancel={onToggleModelHandler}
/>
);
}, [
isFullViewMode,
config,
chartData,
widget.yAxisUnit,
onToggleModelHandler,
]);
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
<Histogram
config={config}
legendConfig={{
position: widget?.legendPosition ?? LegendPosition.BOTTOM,
}}
plotRef={(plot: uPlot | null): void => {
uPlotRef.current = plot;
}}
onDestroy={(): void => {
uPlotRef.current = null;
}}
isQueriesMerged={widget.mergeAllActiveQueries}
yAxisUnit={widget.yAxisUnit}
decimalPrecision={widget.decimalPrecision}
syncMode={DashboardCursorSync.Crosshair}
timezone={timezone.value}
data={chartData as uPlot.AlignedData}
width={containerDimensions.width}
height={containerDimensions.height}
layoutChildren={layoutChildren}
/>
)}
</div>
);
}
export default HistogramPanel;

View File

@@ -1,214 +0,0 @@
import { histogramBucketSizes } from '@grafana/data';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import { DrawStyle, VisibilityMode } from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { incrRoundDn } from 'lib/uPlotV2/utils/scale';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { AlignedData } from 'uplot';
import { PanelMode } from '../types';
import { buildBaseConfig } from '../utils/baseConfigBuilder';
import {
addNullToFirstHistogram,
histogram,
join,
replaceUndefinedWithNull,
roundDecimals,
} from '../utils/histogram';
export interface PrepareHistogramPanelDataParams {
apiResponse: MetricRangePayloadProps;
bucketWidth?: number;
bucketCount?: number;
mergeAllActiveQueries?: boolean;
}
const BUCKET_OFFSET = 0;
const HIST_SORT = (a: number, b: number): number => a - b;
function extractNumericValues(
result: MetricRangePayloadProps['data']['result'],
): number[] {
const values: number[] = [];
for (const item of result) {
for (const [, valueStr] of item.values) {
values.push(Number.parseFloat(valueStr) || 0);
}
}
return values;
}
function computeSmallestDelta(sortedValues: number[]): number {
if (sortedValues.length <= 1) {
return 0;
}
let smallest = Infinity;
for (let i = 1; i < sortedValues.length; i++) {
const delta = sortedValues[i] - sortedValues[i - 1];
if (delta > 0) {
smallest = Math.min(smallest, delta);
}
}
return smallest === Infinity ? 0 : smallest;
}
function selectBucketSize({
range,
bucketCount,
smallestDelta,
bucketWidthOverride,
}: {
range: number;
bucketCount: number;
smallestDelta: number;
bucketWidthOverride?: number;
}): number {
if (bucketWidthOverride != null && bucketWidthOverride > 0) {
return bucketWidthOverride;
}
const targetSize = range / bucketCount;
for (const candidate of histogramBucketSizes) {
if (targetSize < candidate && candidate >= smallestDelta) {
return candidate;
}
}
return 0;
}
function buildFrames(
result: MetricRangePayloadProps['data']['result'],
mergeAllActiveQueries: boolean,
): number[][] {
const frames: number[][] = result.map((item) =>
item.values.map(([, valueStr]) => Number.parseFloat(valueStr) || 0),
);
if (mergeAllActiveQueries && frames.length > 1) {
const first = frames[0];
for (let i = 1; i < frames.length; i++) {
first.push(...frames[i]);
frames[i] = [];
}
}
return frames;
}
export function prepareHistogramPanelData({
apiResponse,
bucketWidth,
bucketCount: bucketCountProp = DEFAULT_BUCKET_COUNT,
mergeAllActiveQueries = false,
}: PrepareHistogramPanelDataParams): AlignedData {
const bucketCount = bucketCountProp ?? DEFAULT_BUCKET_COUNT;
const result = apiResponse.data.result;
const seriesValues = extractNumericValues(result);
if (seriesValues.length === 0) {
return [[]];
}
const sorted = [...seriesValues].sort((a, b) => a - b);
const min = sorted[0];
const max = sorted[sorted.length - 1];
const range = max - min;
const smallestDelta = computeSmallestDelta(sorted);
let bucketSize = selectBucketSize({
range,
bucketCount,
smallestDelta,
bucketWidthOverride: bucketWidth,
});
if (bucketSize <= 0) {
bucketSize = range > 0 ? range / bucketCount : 1;
}
const getBucket = (v: number): number =>
roundDecimals(incrRoundDn(v - BUCKET_OFFSET, bucketSize) + BUCKET_OFFSET, 9);
const frames = buildFrames(result, mergeAllActiveQueries);
const histograms: AlignedData[] = frames
.filter((frame) => frame.length > 0)
.map((frame) => histogram(frame, getBucket, HIST_SORT));
if (histograms.length === 0) {
return [[]];
}
const joined = join(histograms);
replaceUndefinedWithNull(joined);
addNullToFirstHistogram(joined, bucketSize);
return joined;
}
export function prepareHistogramPanelConfig({
widget,
apiResponse,
panelMode,
isDarkMode,
}: {
widget: Widgets;
apiResponse: MetricRangePayloadProps;
panelMode: PanelMode;
isDarkMode: boolean;
}): UPlotConfigBuilder {
const builder = buildBaseConfig({
widget,
isDarkMode,
apiResponse,
panelMode,
panelType: PANEL_TYPES.HISTOGRAM,
});
builder.setCursor({
drag: {
x: false,
y: false,
setScale: true,
},
focus: {
prox: 1e3,
},
});
builder.addScale({
scaleKey: 'x',
time: false,
auto: true,
});
builder.addScale({
scaleKey: 'y',
time: false,
auto: true,
});
const currentQuery = widget.query;
apiResponse.data.result.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,
series.queryName || '', // query
series.legend || '',
);
const label = currentQuery
? getLegend(series, currentQuery, baseLabelName)
: baseLabelName;
builder.addSeries({
label: label,
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
panelType: PANEL_TYPES.HISTOGRAM,
colorMapping: widget.customLegendColors ?? {},
spanGaps: false,
barWidthFactor: 1,
showPoints: VisibilityMode.Never,
pointSize: 5,
isDarkMode,
});
});
return builder;
}

View File

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

View File

@@ -19,9 +19,9 @@ export interface BaseConfigBuilderProps {
widget: Widgets;
apiResponse: MetricRangePayloadProps;
isDarkMode: boolean;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect?: (startTime: number, endTime: number) => void;
timezone?: Timezone;
onClick: OnClickPluginOpts['onClick'];
onDragSelect: (startTime: number, endTime: number) => void;
timezone: Timezone;
panelMode: PanelMode;
panelType: PANEL_TYPES;
minTimeScale?: number;
@@ -40,10 +40,8 @@ export function buildBaseConfig({
minTimeScale,
maxTimeScale,
}: BaseConfigBuilderProps): UPlotConfigBuilder {
const tzDate = timezone
? (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value)
: undefined;
const tzDate = (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value);
const builder = new UPlotConfigBuilder({
onDragSelect,

View File

@@ -1,181 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable no-param-reassign */
import {
NULL_EXPAND,
NULL_REMOVE,
NULL_RETAIN,
} from 'container/PanelWrapper/constants';
import { AlignedData } from 'uplot';
export function incrRoundDn(num: number, incr: number): number {
return Math.floor(num / incr) * incr;
}
export function roundDecimals(val: number, dec = 0): number {
if (Number.isInteger(val)) {
return val;
}
const p = 10 ** dec;
const n = val * p * (1 + Number.EPSILON);
return Math.round(n) / p;
}
function nullExpand(
yVals: Array<number | null>,
nullIdxs: number[],
alignedLen: number,
): void {
for (let i = 0, xi, lastNullIdx = -1; i < nullIdxs.length; i++) {
const nullIdx = nullIdxs[i];
if (nullIdx > lastNullIdx) {
xi = nullIdx - 1;
while (xi >= 0 && yVals[xi] == null) {
yVals[xi--] = null;
}
xi = nullIdx + 1;
while (xi < alignedLen && yVals[xi] == null) {
yVals[(lastNullIdx = xi++)] = null;
}
}
}
}
export function join(
tables: AlignedData[],
nullModes?: number[][],
): AlignedData {
let xVals: Set<number>;
// eslint-disable-next-line prefer-const
xVals = new Set();
for (let ti = 0; ti < tables.length; ti++) {
const t = tables[ti];
const xs = t[0];
const len = xs.length;
for (let i = 0; i < len; i++) {
xVals.add(xs[i]);
}
}
const data = [Array.from(xVals).sort((a, b) => a - b)];
const alignedLen = data[0].length;
const xIdxs = new Map();
for (let i = 0; i < alignedLen; i++) {
xIdxs.set(data[0][i], i);
}
for (let ti = 0; ti < tables.length; ti++) {
const t = tables[ti];
const xs = t[0];
for (let si = 1; si < t.length; si++) {
const ys = t[si];
const yVals = Array(alignedLen).fill(undefined);
const nullMode = nullModes ? nullModes[ti][si] : NULL_RETAIN;
const nullIdxs = [];
for (let i = 0; i < ys.length; i++) {
const yVal = ys[i];
const alignedIdx = xIdxs.get(xs[i]);
if (yVal === null) {
if (nullMode !== NULL_REMOVE) {
yVals[alignedIdx] = yVal;
if (nullMode === NULL_EXPAND) {
nullIdxs.push(alignedIdx);
}
}
} else {
yVals[alignedIdx] = yVal;
}
}
nullExpand(yVals, nullIdxs, alignedLen);
data.push(yVals);
}
}
return data as AlignedData;
}
export function histogram(
vals: number[],
getBucket: (v: number) => number,
sort?: ((a: number, b: number) => number) | null,
): AlignedData {
const hist = new Map();
for (let i = 0; i < vals.length; i++) {
let v = vals[i];
if (v != null) {
v = getBucket(v);
}
const entry = hist.get(v);
if (entry) {
entry.count++;
} else {
hist.set(v, { value: v, count: 1 });
}
}
const bins = [...hist.values()];
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
sort && bins.sort((a, b) => sort(a.value, b.value));
const values = Array(bins.length);
const counts = Array(bins.length);
for (let i = 0; i < bins.length; i++) {
values[i] = bins[i].value;
counts[i] = bins[i].count;
}
return [values, counts];
}
export function replaceUndefinedWithNull(data: AlignedData): AlignedData {
const arrays = data as (number | null | undefined)[][];
for (let i = 0; i < arrays.length; i++) {
for (let j = 0; j < arrays[i].length; j++) {
if (arrays[i][j] === undefined) {
arrays[i][j] = null;
}
}
}
return data;
}
export function addNullToFirstHistogram(
data: AlignedData,
bucketSize: number,
): void {
const histograms = data as (number | null)[][];
if (
histograms.length > 0 &&
histograms[0].length > 0 &&
histograms[0][0] !== null
) {
histograms[0].unshift(histograms[0][0] - bucketSize);
for (let i = 1; i < histograms.length; i++) {
histograms[i].unshift(null);
}
}
}

View File

@@ -477,47 +477,67 @@
}
}
.opentelemetry-radio-container {
.observability-tools-radio-container {
display: flex;
flex-wrap: wrap;
gap: 0 12px;
width: 528px;
.opentelemetry-radio-group {
width: 100%;
.observability-tool-radio-item {
display: flex;
align-items: center;
gap: 8px;
height: 32px;
width: calc((528px - 12px) / 2);
flex: 0 0 calc((528px - 12px) / 2);
.opentelemetry-radio-items-wrapper {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 12px;
width: 100%;
label {
color: var(--l1-foreground);
font-size: 13px;
cursor: pointer;
}
.opentelemetry-radio-item {
display: flex;
align-items: center;
gap: 8px;
height: 32px;
width: calc((528px - 12px) / 2);
min-width: 258px;
flex: 0 0 calc((528px - 12px) / 2);
button[role='radio'] {
&[data-state='unchecked'] {
border-color: var(--l3-border) !important;
border-width: 1px !important;
}
}
&.observability-tool-others-item {
.onboarding-questionaire-other-input {
flex: 1;
}
}
}
}
.migration-timeline-radio-container,
.opentelemetry-radio-container {
display: flex;
flex-wrap: wrap;
gap: 0 12px;
width: 528px;
.migration-timeline-radio-item,
.opentelemetry-radio-item {
display: flex;
align-items: center;
gap: 8px;
height: 32px;
width: calc((528px - 12px) / 2);
flex: 0 0 calc((528px - 12px) / 2);
label {
color: var(--l1-foreground);
font-family: Inter, sans-serif;
font-size: 13px;
font-weight: 400;
line-height: 1;
letter-spacing: -0.065px;
box-sizing: border-box;
cursor: pointer;
}
.ant-radio {
.ant-radio-inner {
width: 16px;
height: 16px;
border-color: var(--l3-border);
}
&.ant-radio-checked .ant-radio-inner {
border-color: var(--bg-robin-500);
background-color: var(--bg-robin-500);
}
button[role='radio'] {
&[data-state='unchecked'] {
border-color: var(--l3-border) !important;
border-width: 1px !important;
}
}
}
@@ -977,27 +997,6 @@
color: var(--bg-slate-300);
}
.opentelemetry-radio-container {
.opentelemetry-radio-group {
.opentelemetry-radio-items-wrapper {
.opentelemetry-radio-item {
color: var(--l1-foreground);
.ant-radio {
.ant-radio-inner {
border-color: var(--l3-border);
}
&.ant-radio-checked .ant-radio-inner {
border-color: var(--bg-robin-500);
background-color: var(--bg-robin-500);
}
}
}
}
}
}
.onboarding-back-button {
border-color: var(--text-vanilla-300);
color: var(--l3-foreground);

View File

@@ -1,34 +1,27 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@signozhq/button';
import { Checkbox } from '@signozhq/checkbox';
import { Input } from '@signozhq/input';
import { Radio, Typography } from 'antd';
import { RadioChangeEvent } from 'antd/es/radio';
import {
RadioGroup,
RadioGroupItem,
RadioGroupLabel,
} from '@signozhq/radio-group';
import { Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import editOrg from 'api/organization/editOrg';
import { useNotifications } from 'hooks/useNotifications';
import { ArrowRight, Loader2 } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { ArrowRight } from 'lucide-react';
import '../OnboardingQuestionaire.styles.scss';
export interface OrgData {
id: string;
displayName: string;
}
export interface OrgDetails {
organisationName: string;
usesObservability: boolean | null;
observabilityTool: string | null;
otherTool: string | null;
usesOtel: boolean | null;
migrationTimeline: string | null;
}
interface OrgQuestionsProps {
currentOrgData: OrgData | null;
orgDetails: OrgDetails;
onNext: (details: OrgDetails) => void;
}
@@ -45,19 +38,14 @@ const observabilityTools = {
Others: 'Others',
};
function OrgQuestions({
currentOrgData,
orgDetails,
onNext,
}: OrgQuestionsProps): JSX.Element {
const { updateOrg } = useAppContext();
const { notifications } = useNotifications();
const migrationTimelineOptions = {
lessThanMonth: 'Less than a month',
oneToThreeMonths: '1-3 months',
greaterThanThreeMonths: 'Greater than 3 months',
justExploring: 'Just exploring',
};
const { t } = useTranslation(['organizationsettings', 'common']);
const [organisationName, setOrganisationName] = useState<string>(
orgDetails?.organisationName || '',
);
function OrgQuestions({ orgDetails, onNext }: OrgQuestionsProps): JSX.Element {
const [observabilityTool, setObservabilityTool] = useState<string | null>(
orgDetails?.observabilityTool || null,
);
@@ -66,92 +54,33 @@ function OrgQuestions({
);
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
useEffect(() => {
setOrganisationName(orgDetails.organisationName);
}, [orgDetails.organisationName]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [usesOtel, setUsesOtel] = useState<boolean | null>(orgDetails.usesOtel);
const [migrationTimeline, setMigrationTimeline] = useState<string | null>(
orgDetails?.migrationTimeline || null,
);
const handleOrgNameUpdate = async (): Promise<void> => {
const showMigrationQuestion =
observabilityTool !== null && observabilityTool !== 'None';
const handleNext = (): void => {
const usesObservability =
!observabilityTool?.includes('None') && observabilityTool !== null;
/* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */
if (
!currentOrgData ||
!organisationName ||
organisationName === '' ||
orgDetails.organisationName === organisationName
) {
logEvent('Org Onboarding: Answered', {
usesObservability,
observabilityTool,
otherTool,
usesOtel,
});
logEvent('Org Onboarding: Answered', {
usesObservability,
observabilityTool,
otherTool,
usesOtel,
migrationTimeline,
});
onNext({
organisationName,
usesObservability,
observabilityTool,
otherTool,
usesOtel,
});
return;
}
try {
setIsLoading(true);
const { statusCode, error } = await editOrg({
displayName: organisationName,
orgId: currentOrgData.id,
});
if (statusCode === 204) {
updateOrg(currentOrgData?.id, organisationName);
logEvent('Org Onboarding: Org Name Updated', {
organisationName,
});
logEvent('Org Onboarding: Answered', {
usesObservability,
observabilityTool,
otherTool,
usesOtel,
});
onNext({
organisationName,
usesObservability,
observabilityTool,
otherTool,
usesOtel,
});
} else {
logEvent('Org Onboarding: Org Name Update Failed', {
organisationName: orgDetails.organisationName,
});
notifications.error({
message:
error ||
t('something_went_wrong', {
ns: 'common',
}),
});
}
setIsLoading(false);
} catch (error) {
setIsLoading(false);
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
});
}
onNext({
usesObservability,
observabilityTool,
otherTool,
usesOtel,
migrationTimeline,
});
};
const isValidUsesObservability = (): boolean => {
@@ -173,22 +102,29 @@ function OrgQuestions({
useEffect(() => {
const isValidObservability = isValidUsesObservability();
const isMigrationValid = !showMigrationQuestion || migrationTimeline !== null;
if (organisationName !== '' && usesOtel !== null && isValidObservability) {
if (usesOtel !== null && isValidObservability && isMigrationValid) {
setIsNextDisabled(false);
} else {
setIsNextDisabled(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [organisationName, usesOtel, observabilityTool, otherTool]);
}, [
usesOtel,
observabilityTool,
otherTool,
migrationTimeline,
showMigrationQuestion,
]);
const createObservabilityToolHandler = (tool: string) => (
checked: boolean,
): void => {
if (checked) {
setObservabilityTool(tool);
} else if (observabilityTool === tool) {
setObservabilityTool(null);
const handleObservabilityToolChange = (value: string): void => {
setObservabilityTool(value);
if (value !== 'Others') {
setOtherTool('');
}
if (value === 'None') {
setMigrationTimeline(null);
}
};
@@ -196,10 +132,6 @@ function OrgQuestions({
setUsesOtel(value === 'yes');
};
const handleOnNext = (): void => {
handleOrgNameUpdate();
};
return (
<div className="questions-container">
<div className="onboarding-header-section">
@@ -214,40 +146,24 @@ function OrgQuestions({
<div className="questions-form-container">
<div className="questions-form">
<div className="form-group">
<label className="question" htmlFor="organisationName">
Name of your company
</label>
<Input
type="text"
name="organisationName"
id="organisationName"
placeholder="e.g. Simpsonville"
autoComplete="off"
value={organisationName}
onChange={(e): void => setOrganisationName(e.target.value)}
/>
</div>
<div className="form-group">
<label className="question" htmlFor="observabilityTool">
Which observability tool do you currently use?
</label>
<div className="observability-tools-checkbox-container">
<RadioGroup
value={observabilityTool || ''}
onValueChange={handleObservabilityToolChange}
className="observability-tools-radio-container"
>
{Object.entries(observabilityTools).map(([tool, label]) => {
if (tool === 'Others') {
return (
<div
key={tool}
className="checkbox-item observability-tool-checkbox-item observability-tool-others-item"
className="radio-item observability-tool-radio-item observability-tool-others-item"
>
<Checkbox
id={`checkbox-${tool}`}
checked={observabilityTool === tool}
onCheckedChange={createObservabilityToolHandler(tool)}
labelName={observabilityTool === 'Others' ? '' : label}
/>
{observabilityTool === 'Others' && (
<RadioGroupItem value={tool} id={`radio-${tool}`} />
{observabilityTool === 'Others' ? (
<Input
type="text"
className="onboarding-questionaire-other-input"
@@ -256,55 +172,60 @@ function OrgQuestions({
autoFocus
onChange={(e): void => setOtherTool(e.target.value)}
/>
) : (
<RadioGroupLabel htmlFor={`radio-${tool}`}>{label}</RadioGroupLabel>
)}
</div>
);
}
return (
<div
key={tool}
className="checkbox-item observability-tool-checkbox-item"
>
<Checkbox
id={`checkbox-${tool}`}
checked={observabilityTool === tool}
onCheckedChange={createObservabilityToolHandler(tool)}
labelName={label}
/>
<div key={tool} className="radio-item observability-tool-radio-item">
<RadioGroupItem value={tool} id={`radio-${tool}`} />
<RadioGroupLabel htmlFor={`radio-${tool}`}>{label}</RadioGroupLabel>
</div>
);
})}
</div>
</RadioGroup>
</div>
{showMigrationQuestion && (
<div className="form-group">
<div className="question">
What is your timeline for migrating to SigNoz?
</div>
<RadioGroup
value={migrationTimeline || ''}
onValueChange={setMigrationTimeline}
className="migration-timeline-radio-container"
>
{Object.entries(migrationTimelineOptions).map(([key, label]) => (
<div key={key} className="radio-item migration-timeline-radio-item">
<RadioGroupItem value={key} id={`radio-migration-${key}`} />
<RadioGroupLabel htmlFor={`radio-migration-${key}`}>
{label}
</RadioGroupLabel>
</div>
))}
</RadioGroup>
</div>
)}
<div className="form-group">
<div className="question">Do you already use OpenTelemetry?</div>
<div className="opentelemetry-radio-container">
<Radio.Group
value={((): string | undefined => {
if (usesOtel === true) {
return 'yes';
}
if (usesOtel === false) {
return 'no';
}
return undefined;
})()}
onChange={(e: RadioChangeEvent): void =>
handleOtelChange(e.target.value)
}
className="opentelemetry-radio-group"
>
<div className="opentelemetry-radio-items-wrapper">
<Radio value="yes" className="opentelemetry-radio-item">
Yes
</Radio>
<Radio value="no" className="opentelemetry-radio-item">
No
</Radio>
</div>
</Radio.Group>
</div>
<RadioGroup
value={usesOtel === true ? 'yes' : usesOtel === false ? 'no' : ''}
onValueChange={handleOtelChange}
className="opentelemetry-radio-container"
>
<div className="radio-item opentelemetry-radio-item">
<RadioGroupItem value="yes" id="radio-otel-yes" />
<RadioGroupLabel htmlFor="radio-otel-yes">Yes</RadioGroupLabel>
</div>
<div className="radio-item opentelemetry-radio-item">
<RadioGroupItem value="no" id="radio-otel-no" />
<RadioGroupLabel htmlFor="radio-otel-no">No</RadioGroupLabel>
</div>
</RadioGroup>
</div>
</div>
@@ -312,15 +233,9 @@ function OrgQuestions({
variant="solid"
color="primary"
className={`onboarding-next-button ${isNextDisabled ? 'disabled' : ''}`}
onClick={handleOnNext}
onClick={handleNext}
disabled={isNextDisabled}
suffixIcon={
isLoading ? (
<Loader2 className="animate-spin" size={12} />
) : (
<ArrowRight size={12} />
)
}
suffixIcon={<ArrowRight size={12} />}
>
Next
</Button>

View File

@@ -69,7 +69,7 @@ describe('OnboardingQuestionaire Component', () => {
render(<OnboardingQuestionaire />);
expect(screen.getByText(/welcome to signoz cloud/i)).toBeInTheDocument();
expect(screen.getByLabelText(/name of your company/i)).toBeInTheDocument();
expect(
screen.getByText(/which observability tool do you currently use/i),
).toBeInTheDocument();
@@ -86,15 +86,12 @@ describe('OnboardingQuestionaire Component', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<OnboardingQuestionaire />);
const orgNameInput = screen.getByLabelText(/name of your company/i);
await user.clear(orgNameInput);
await user.type(orgNameInput, 'Test Company');
const datadogCheckbox = screen.getByLabelText(/datadog/i);
await user.click(datadogCheckbox);
const otelYes = screen.getByRole('radio', { name: /yes/i });
await user.click(otelYes);
await user.click(screen.getByLabelText(/just exploring/i));
const nextButton = await screen.findByRole('button', { name: /next/i });
expect(nextButton).not.toBeDisabled();
@@ -112,15 +109,38 @@ describe('OnboardingQuestionaire Component', () => {
).toBeInTheDocument();
});
it('shows migration timeline options only when specific observability tools are selected', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<OnboardingQuestionaire />);
// Initially not visible
expect(
screen.queryByText(/What is your timeline for migrating to SigNoz/i),
).not.toBeInTheDocument();
const datadogCheckbox = screen.getByLabelText(/datadog/i);
await user.click(datadogCheckbox);
expect(
await screen.findByText(/What is your timeline for migrating to SigNoz/i),
).toBeInTheDocument();
// Not visible when None is selected
const noneCheckbox = screen.getByLabelText(/none\/starting fresh/i);
await user.click(noneCheckbox);
expect(
screen.queryByText(/What is your timeline for migrating to SigNoz/i),
).not.toBeInTheDocument();
});
it('proceeds to step 2 when next is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<OnboardingQuestionaire />);
const orgNameInput = screen.getByLabelText(/name of your company/i);
await user.clear(orgNameInput);
await user.type(orgNameInput, 'Test Company');
await user.click(screen.getByLabelText(/datadog/i));
await user.click(screen.getByRole('radio', { name: /yes/i }));
await user.click(screen.getByLabelText(/just exploring/i));
const nextButton = screen.getByRole('button', { name: /next/i });
await user.click(nextButton);
@@ -137,11 +157,10 @@ describe('OnboardingQuestionaire Component', () => {
render(<OnboardingQuestionaire />);
// Navigate to step 2
const orgNameInput = screen.getByLabelText(/name of your company/i);
await user.clear(orgNameInput);
await user.type(orgNameInput, 'Test Company');
await user.click(screen.getByLabelText(/datadog/i));
await user.click(screen.getByRole('radio', { name: /yes/i }));
await user.click(screen.getByLabelText(/just exploring/i));
await user.click(screen.getByRole('button', { name: /next/i }));
expect(
@@ -157,11 +176,10 @@ describe('OnboardingQuestionaire Component', () => {
render(<OnboardingQuestionaire />);
// Navigate to step 2
const orgNameInput = screen.getByLabelText(/name of your company/i);
await user.clear(orgNameInput);
await user.type(orgNameInput, 'Test Company');
await user.click(screen.getByLabelText(/datadog/i));
await user.click(screen.getByRole('radio', { name: /yes/i }));
await user.click(screen.getByLabelText(/just exploring/i));
await user.click(screen.getByRole('button', { name: /next/i }));
await waitFor(() => {
@@ -175,11 +193,10 @@ describe('OnboardingQuestionaire Component', () => {
render(<OnboardingQuestionaire />);
// Navigate to step 2
const orgNameInput = screen.getByLabelText(/name of your company/i);
await user.clear(orgNameInput);
await user.type(orgNameInput, 'Test Company');
await user.click(screen.getByLabelText(/datadog/i));
await user.click(screen.getByRole('radio', { name: /yes/i }));
await user.click(screen.getByLabelText(/just exploring/i));
await user.click(screen.getByRole('button', { name: /next/i }));
expect(
@@ -203,11 +220,10 @@ describe('OnboardingQuestionaire Component', () => {
render(<OnboardingQuestionaire />);
// Navigate to step 2
const orgNameInput = screen.getByLabelText(/name of your company/i);
await user.clear(orgNameInput);
await user.type(orgNameInput, 'Test Company');
await user.click(screen.getByLabelText(/datadog/i));
await user.click(screen.getByRole('radio', { name: /yes/i }));
await user.click(screen.getByLabelText(/just exploring/i));
await user.click(screen.getByRole('button', { name: /next/i }));
expect(
@@ -232,11 +248,10 @@ describe('OnboardingQuestionaire Component', () => {
render(<OnboardingQuestionaire />);
// Navigate through steps 1 and 2
const orgNameInput = screen.getByLabelText(/name of your company/i);
await user.clear(orgNameInput);
await user.type(orgNameInput, 'Test Company');
await user.click(screen.getByLabelText(/datadog/i));
await user.click(screen.getByRole('radio', { name: /yes/i }));
await user.click(screen.getByLabelText(/just exploring/i));
await user.click(screen.getByRole('button', { name: /next/i }));
expect(
@@ -267,11 +282,10 @@ describe('OnboardingQuestionaire Component', () => {
render(<OnboardingQuestionaire />);
// Navigate to step 3
const orgNameInput = screen.getByLabelText(/name of your company/i);
await user.clear(orgNameInput);
await user.type(orgNameInput, 'Test Company');
await user.click(screen.getByLabelText(/datadog/i));
await user.click(screen.getByRole('radio', { name: /yes/i }));
await user.click(screen.getByLabelText(/just exploring/i));
await user.click(screen.getByRole('button', { name: /next/i }));
expect(
@@ -290,40 +304,4 @@ describe('OnboardingQuestionaire Component', () => {
).toBeInTheDocument();
});
});
describe('Error Handling', () => {
it('handles organization update error gracefully', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.put(EDIT_ORG_ENDPOINT, (_, res, ctx) =>
res(
ctx.status(500),
ctx.json({
error: {
code: 'INTERNAL_ERROR',
message: 'Failed to update organization',
},
}),
),
),
);
render(<OnboardingQuestionaire />);
const orgNameInput = screen.getByLabelText(/name of your company/i);
await user.clear(orgNameInput);
await user.type(orgNameInput, 'Test Company');
await user.click(screen.getByLabelText(/datadog/i));
await user.click(screen.getByRole('radio', { name: /yes/i }));
const nextButton = screen.getByRole('button', { name: /next/i });
await user.click(nextButton);
// Component should still be functional
await waitFor(() => {
expect(nextButton).not.toBeDisabled();
});
});
});
});

View File

@@ -23,7 +23,7 @@ import InviteTeamMembers from './InviteTeamMembers/InviteTeamMembers';
import OptimiseSignozNeeds, {
OptimiseSignozDetails,
} from './OptimiseSignozNeeds/OptimiseSignozNeeds';
import OrgQuestions, { OrgData, OrgDetails } from './OrgQuestions/OrgQuestions';
import OrgQuestions, { OrgDetails } from './OrgQuestions/OrgQuestions';
import './OnboardingQuestionaire.styles.scss';
@@ -37,11 +37,11 @@ export const showErrorNotification = (
};
const INITIAL_ORG_DETAILS: OrgDetails = {
organisationName: '',
usesObservability: true,
observabilityTool: '',
otherTool: '',
usesOtel: null,
migrationTimeline: null,
};
const INITIAL_SIGNOZ_DETAILS: SignozDetails = {
@@ -79,25 +79,11 @@ function OnboardingQuestionaire(): JSX.Element {
InviteTeamMembersProps[] | null
>(null);
const [currentOrgData, setCurrentOrgData] = useState<OrgData | null>(null);
const [
updatingOrgOnboardingStatus,
setUpdatingOrgOnboardingStatus,
] = useState<boolean>(false);
useEffect(() => {
if (org) {
setCurrentOrgData(org[0]);
setOrgDetails({
...orgDetails,
organisationName: org[0].displayName,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [org]);
useEffect(() => {
logEvent('Org Onboarding: Started', {
org_id: org?.[0]?.id,
@@ -175,6 +161,7 @@ function OnboardingQuestionaire(): JSX.Element {
? (orgDetails?.otherTool as string)
: (orgDetails?.observabilityTool as string),
where_did_you_discover_signoz: signozDetails?.discoverSignoz as string,
timeline_for_migrating_to_signoz: orgDetails?.migrationTimeline as string,
reasons_for_interest_in_signoz: signozDetails?.interestInSignoz?.includes(
'Others',
)
@@ -208,7 +195,6 @@ function OnboardingQuestionaire(): JSX.Element {
<div className="onboarding-questionaire-content">
{currentStep === 1 && (
<OrgQuestions
currentOrgData={currentOrgData}
orgDetails={{
...orgDetails,
usesOtel: orgDetails.usesOtel ?? null,

View File

@@ -1,8 +0,0 @@
import { HistogramTooltipProps } from '../types';
import Tooltip from './Tooltip';
export default function HistogramTooltip(
props: HistogramTooltipProps,
): JSX.Element {
return <Tooltip {...props} showTooltipHeader={false} />;
}

View File

@@ -16,16 +16,12 @@ export default function Tooltip({
uPlotInstance,
timezone,
content,
showTooltipHeader = true,
}: TooltipProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const tooltipContent = content ?? [];
const headerTitle = useMemo(() => {
if (!showTooltipHeader) {
return null;
}
const data = uPlotInstance.data;
const cursorIdx = uPlotInstance.cursor.idx;
if (cursorIdx == null) {
@@ -34,12 +30,7 @@ export default function Tooltip({
return dayjs(data[0][cursorIdx] * 1000)
.tz(timezone)
.format(DATE_TIME_FORMATS.MONTH_DATETIME_SECONDS);
}, [
timezone,
uPlotInstance.data,
uPlotInstance.cursor.idx,
showTooltipHeader,
]);
}, [timezone, uPlotInstance.data, uPlotInstance.cursor.idx]);
return (
<div
@@ -48,11 +39,9 @@ export default function Tooltip({
isDarkMode ? 'darkMode' : 'lightMode',
)}
>
{showTooltipHeader && (
<div className="uplot-tooltip-header">
<span>{headerTitle}</span>
</div>
)}
<div className="uplot-tooltip-header">
<span>{headerTitle}</span>
</div>
<div
style={{
height: Math.min(

View File

@@ -60,7 +60,6 @@ export interface TooltipRenderArgs {
}
export interface BaseTooltipProps {
showTooltipHeader?: boolean;
timezone: string;
yAxisUnit?: string;
decimalPrecision?: PrecisionOption;
@@ -75,14 +74,7 @@ export interface BarTooltipProps extends BaseTooltipProps, TooltipRenderArgs {
isStackedBarChart?: boolean;
}
export interface HistogramTooltipProps
extends BaseTooltipProps,
TooltipRenderArgs {}
export type TooltipProps =
| TimeSeriesTooltipProps
| BarTooltipProps
| HistogramTooltipProps;
export type TooltipProps = TimeSeriesTooltipProps | BarTooltipProps;
export enum LegendPosition {
BOTTOM = 'bottom',

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,4 +7,5 @@ export interface UpdateProfileProps {
number_of_services: number;
number_of_hosts: number;
where_did_you_discover_signoz: string;
timeline_for_migrating_to_signoz: string;
}

View File

@@ -4073,6 +4073,16 @@
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-collection@1.1.7":
version "1.1.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz#d05c25ca9ac4695cc19ba91f42f686e3ea2d9aec"
integrity sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==
dependencies:
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-compose-refs@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
@@ -4159,6 +4169,11 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-direction@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14"
integrity sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==
"@radix-ui/react-dismissable-layer@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz#35b7826fa262fd84370faef310e627161dffa76b"
@@ -4387,6 +4402,22 @@
dependencies:
"@radix-ui/react-slot" "1.2.4"
"@radix-ui/react-radio-group@^1.3.4":
version "1.3.8"
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz#93f102b5b948d602c2f2adb1bc5c347cbaf64bd9"
integrity sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-direction" "1.1.1"
"@radix-ui/react-presence" "1.1.5"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-roving-focus" "1.1.11"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-use-previous" "1.1.1"
"@radix-ui/react-use-size" "1.1.1"
"@radix-ui/react-roving-focus@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974"
@@ -4403,6 +4434,21 @@
"@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-roving-focus@1.1.11":
version "1.1.11"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz#ef54384b7361afc6480dcf9907ef2fedb5080fd9"
integrity sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-collection" "1.1.7"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-direction" "1.1.1"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-slot@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.0.tgz#7fa805b99891dea1e862d8f8fbe07f4d6d0fd698"
@@ -5075,6 +5121,20 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/radio-group@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/radio-group/-/radio-group-0.0.2.tgz#4b13567bfee2645226f2cf41f261bbb288e1be4b"
integrity sha512-ahykmA5hPujOC964CFveMlQ12tWSyut2CUiFRqT1QxRkOLS2R44Qn2hh2psqJJ18JMX/24ZYCAIh9Bdd5XW+7g==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-radio-group" "^1.3.4"
"@radix-ui/react-slot" "^1.1.0"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/resizable@0.0.0":
version "0.0.0"
resolved "https://registry.yarnpkg.com/@signozhq/resizable/-/resizable-0.0.0.tgz#a517818b9f9bcdaeafc55ae134be86522bc90e9f"