Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pretty-donkeys-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---

fix: Ensure displayed queries and MV indicators match queried configs
61 changes: 50 additions & 11 deletions packages/app/src/ChartUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { add } from 'date-fns';
import { omit } from 'lodash';
import SqlString from 'sqlstring';
import { z } from 'zod';
import {
Expand All @@ -13,6 +14,7 @@ import { isMetricChartConfig } from '@hyperdx/common-utils/dist/core/renderChart
import {
AggregateFunction as AggFnV2,
ChartConfigWithDateRange,
ChartConfigWithOptTimestamp,
DisplayType,
Filter,
MetricsDataType as MetricsDataTypeV2,
Expand Down Expand Up @@ -139,24 +141,32 @@ export const isGranularity = (value: string): value is Granularity => {
return Object.values(Granularity).includes(value as Granularity);
};

export function useTimeChartSettings(chartConfig: ChartConfigWithDateRange) {
const autoGranularity = useMemo(() => {
return convertDateRangeToGranularityString(chartConfig.dateRange, 80);
}, [chartConfig.dateRange]);

export function convertToTimeChartConfig(config: ChartConfigWithDateRange) {
const granularity =
chartConfig.granularity === 'auto' || chartConfig.granularity == null
? autoGranularity
: chartConfig.granularity;
config.granularity === 'auto' || config.granularity == null
? convertDateRangeToGranularityString(config.dateRange, 80)
: config.granularity;

return {
displayType: chartConfig.displayType,
dateRange: chartConfig.dateRange,
fillNulls: chartConfig.fillNulls,
...config,
granularity,
limit: { limit: 100000 },
};
}

export function useTimeChartSettings(chartConfig: ChartConfigWithDateRange) {
return useMemo(() => {
const convertedConfig = convertToTimeChartConfig(chartConfig);

return {
displayType: convertedConfig.displayType,
dateRange: convertedConfig.dateRange,
fillNulls: convertedConfig.fillNulls,
granularity: convertedConfig.granularity,
};
}, [chartConfig]);
}

export function seriesToSearchQuery({
series,
groupByValue,
Expand Down Expand Up @@ -1149,3 +1159,32 @@ export function buildTableRowSearchUrl({
valueRangeFilter,
});
}

export function convertToNumberChartConfig(
config: ChartConfigWithDateRange,
): ChartConfigWithOptTimestamp {
return omit(config, ['granularity', 'groupBy']);
}

export function convertToTableChartConfig(
config: ChartConfigWithOptTimestamp,
): ChartConfigWithOptTimestamp {
const convertedConfig = structuredClone(omit(config, ['granularity']));

// Set a default limit if not already set
if (!convertedConfig.limit) {
convertedConfig.limit = { limit: 200 };
}

// Set a default orderBy if groupBy is set but orderBy is not,
// so that the set of rows within the limit is stable.
if (
convertedConfig.groupBy &&
typeof convertedConfig.groupBy === 'string' &&
!convertedConfig.orderBy
) {
convertedConfig.orderBy = convertedConfig.groupBy;
}

return convertedConfig;
}
63 changes: 34 additions & 29 deletions packages/app/src/DBDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,13 @@ import { Tags } from './components/Tags';
import useDashboardFilters from './hooks/useDashboardFilters';
import { useDashboardRefresh } from './hooks/useDashboardRefresh';
import { parseAsStringWithNewLines } from './utils/queryParsers';
import api from './api';
import { buildTableRowSearchUrl, DEFAULT_CHART_CONFIG } from './ChartUtils';
import {
buildTableRowSearchUrl,
convertToNumberChartConfig,
convertToTableChartConfig,
convertToTimeChartConfig,
DEFAULT_CHART_CONFIG,
} from './ChartUtils';
import { IS_LOCAL_MODE } from './config';
import { useDashboard } from './dashboard';
import DashboardFilters from './DashboardFilters';
Expand Down Expand Up @@ -120,7 +125,6 @@ const Tile = forwardRef(
onEditClick,
onDeleteClick,
onUpdateChart,
onSettled,
granularity,
onTimeRangeSelect,
filters,
Expand All @@ -132,7 +136,7 @@ const Tile = forwardRef(
onMouseUp,
onTouchEnd,
children,
isHighlighed,
isHighlighted,
}: {
chart: Tile;
dateRange: [Date, Date];
Expand All @@ -153,32 +157,45 @@ const Tile = forwardRef(
onMouseUp?: (e: React.MouseEvent) => void;
onTouchEnd?: (e: React.TouchEvent) => void;
children?: React.ReactNode; // Resizer tooltip
isHighlighed?: boolean;
isHighlighted?: boolean;
},
ref: ForwardedRef<HTMLDivElement>,
) => {
useEffect(() => {
if (isHighlighed) {
if (isHighlighted) {
document
.getElementById(`chart-${chart.id}`)
?.scrollIntoView({ behavior: 'smooth' });
}
}, [chart.id, isHighlighed]);
}, [chart.id, isHighlighted]);

const [queriedConfig, setQueriedConfig] = useState<
ChartConfigWithDateRange | undefined
>(undefined);

// Transform the queried config to match what will be queried by the
// child components, so that the MV Optimization indicator is accurate.
const configForMVOptimizationIndicator = useMemo(() => {
if (!queriedConfig) return undefined;

if (
queriedConfig.displayType === DisplayType.Line ||
queriedConfig.displayType === DisplayType.StackedBar
) {
return convertToTimeChartConfig(queriedConfig);
} else if (queriedConfig.displayType === DisplayType.Number) {
return convertToNumberChartConfig(queriedConfig);
} else if (queriedConfig.displayType === DisplayType.Table) {
return convertToTableChartConfig(queriedConfig);
}

return queriedConfig;
}, [queriedConfig]);

const { data: source } = useSource({
id: chart.config.source,
});

// const prevSource = usePrevious(source);
// const prevChart = usePrevious(chart);
// const prevDateRange = usePrevious(dateRange);
// const prevGranularity = usePrevious(granularity);
// const prevFilters = usePrevious(filters);

useEffect(() => {
if (source != null) {
// TODO: will need to update this when we allow for multiple metrics per chart
Expand Down Expand Up @@ -233,13 +250,11 @@ const Tile = forwardRef(
return tooltip;
}, [alert]);

const { data: me } = api.useMe();

return (
<div
data-testid={`dashboard-tile-${chart.id}`}
className={`p-2 ${className} d-flex flex-column bg-muted rounded ${
isHighlighed && 'dashboard-chart-highlighted'
isHighlighted && 'dashboard-chart-highlighted'
}`}
id={`chart-${chart.id}`}
onMouseEnter={() => setHovered(true)}
Expand Down Expand Up @@ -321,7 +336,7 @@ const Tile = forwardRef(
{source?.materializedViews?.length && queriedConfig && (
<Box onMouseDown={e => e.stopPropagation()}>
<MVOptimizationIndicator
config={queriedConfig}
config={configForMVOptimizationIndicator}
source={source}
variant="icon"
/>
Expand Down Expand Up @@ -446,7 +461,6 @@ const EditTileModal = ({
<EditTimeChartForm
dashboardId={dashboardId}
chartConfig={chart.config}
setChartConfig={config => {}}
dateRange={dateRange}
isSaving={isSaving}
onSave={config => {
Expand Down Expand Up @@ -741,7 +755,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
...(filterQueries ?? []),
]}
onTimeRangeSelect={onTimeRangeSelect}
isHighlighed={highlightedTileId === chart.id}
isHighlighted={highlightedTileId === chart.id}
onUpdateChart={newChart => {
if (!dashboard) {
return;
Expand Down Expand Up @@ -814,15 +828,6 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {

const deleteDashboard = useDeleteDashboard();

// Search tile
const [rowId, setRowId] = useQueryState('rowWhere');
const [rowSource, setRowSource] = useQueryState('rowSource');
const { data: rowSidePanelSource } = useSource({ id: rowSource });
const handleSidePanelClose = useCallback(() => {
setRowId(null);
setRowSource(null);
}, [setRowId, setRowSource]);

const handleUpdateTags = useCallback(
(newTags: string[]) => {
if (dashboard?.id) {
Expand Down Expand Up @@ -1216,7 +1221,7 @@ const DBDashboardPageDynamic = dynamic(async () => DBDashboardPage, {
ssr: false,
});

// @ts-ignore
// @ts-expect-error for getLayout
DBDashboardPageDynamic.getLayout = withAppNav;

export default DBDashboardPageDynamic;
127 changes: 125 additions & 2 deletions packages/app/src/__tests__/ChartUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
import {
ChartConfigWithDateRange,
SourceKind,
TSource,
} from '@hyperdx/common-utils/dist/types';

import { formatResponseForTimeChart } from '@/ChartUtils';
import {
convertToNumberChartConfig,
convertToTableChartConfig,
convertToTimeChartConfig,
formatResponseForTimeChart,
} from '@/ChartUtils';

if (!globalThis.structuredClone) {
globalThis.structuredClone = (obj: any) => {
return JSON.parse(JSON.stringify(obj));
};
}

describe('ChartUtils', () => {
describe('formatResponseForTimeChart', () => {
Expand Down Expand Up @@ -529,4 +544,112 @@ describe('ChartUtils', () => {
]);
});
});

describe('convertToTimeChartConfig', () => {
it('should set granularity when granularity is auto', () => {
const config = {
granularity: 'auto',
dateRange: [
new Date('2025-11-26T00:00:00Z'),
new Date('2025-11-27T00:00:00Z'),
],
} as ChartConfigWithDateRange;

const granularityFromFunction =
convertToTimeChartConfig(config).granularity;

expect(granularityFromFunction).toBe('30 minute');
});

it('should set granularity when granularity is undefined', () => {
const config = {
dateRange: [
new Date('2025-11-26T00:00:00Z'),
new Date('2025-11-27T00:00:00Z'),
],
} as ChartConfigWithDateRange;

const granularityFromFunction =
convertToTimeChartConfig(config).granularity;

expect(granularityFromFunction).toBe('30 minute');
});

it('should retain the specified granularity when not auto', () => {
const config = {
granularity: '5 minute',
dateRange: [
new Date('2025-11-26T00:00:00Z'),
new Date('2025-11-27T00:00:00Z'),
],
} as ChartConfigWithDateRange;

const granularityFromFunction =
convertToTimeChartConfig(config).granularity;

expect(granularityFromFunction).toBe('5 minute');
});
});

describe('convertToNumberChartConfig', () => {
it('should remove granularity and groupBy from the config', () => {
const config = {
granularity: '5 minute',
groupBy: 'ServiceName',
dateRange: [
new Date('2025-11-26T00:00:00Z'),
new Date('2025-11-27T00:00:00Z'),
],
} as ChartConfigWithDateRange;

const convertedConfig = convertToNumberChartConfig(config);

expect(convertedConfig.granularity).toBeUndefined();
expect(convertedConfig.groupBy).toBeUndefined();
});
});

describe('convertToTableChartConfig', () => {
it('should remove granularity from the config', () => {
const config = {
granularity: '5 minute',
dateRange: [
new Date('2025-11-26T00:00:00Z'),
new Date('2025-11-27T00:00:00Z'),
],
} as ChartConfigWithDateRange;

const convertedConfig = convertToTableChartConfig(config);

expect(convertedConfig.granularity).toBeUndefined();
});

it('should apply a default sort if none is provided', () => {
const config = {
groupBy: 'ServiceName',
dateRange: [
new Date('2025-11-26T00:00:00Z'),
new Date('2025-11-27T00:00:00Z'),
],
} as ChartConfigWithDateRange;

const convertedConfig = convertToTableChartConfig(config);

expect(convertedConfig.orderBy).toEqual('ServiceName');
});

it('should apply a default limit if none is provided', () => {
const config = {
groupBy: 'ServiceName',
dateRange: [
new Date('2025-11-26T00:00:00Z'),
new Date('2025-11-27T00:00:00Z'),
],
} as ChartConfigWithDateRange;

const convertedConfig = convertToTableChartConfig(config);

expect(convertedConfig.limit).toEqual({ limit: 200 });
});
});
});
Loading
Loading