diff --git a/.changeset/popular-coins-pretend.md b/.changeset/popular-coins-pretend.md new file mode 100644 index 0000000000..60dd22f6b7 --- /dev/null +++ b/.changeset/popular-coins-pretend.md @@ -0,0 +1,7 @@ +--- +"@hyperdx/common-utils": patch +"@hyperdx/api": patch +"@hyperdx/app": patch +--- + +fix: Fix bugs in the Services dashboard diff --git a/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts b/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts index 36d8cc366c..228cbe035f 100644 --- a/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts +++ b/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts @@ -145,7 +145,7 @@ describe('renderChartConfig', () => { { aggFn: 'sum', valueExpression: 'strVal' }, ], from: { - databaseName: '', + databaseName: DEFAULT_DATABASE, tableName: `agg_fn_str_test`, }, where: '', @@ -183,7 +183,7 @@ describe('renderChartConfig', () => { { aggFn: 'sum', valueExpression: 'strVal' }, ], from: { - databaseName: '', + databaseName: DEFAULT_DATABASE, tableName: `agg_fn_default_test`, }, where: '', diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index 9fb3eafd19..43db4794e3 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -7,7 +7,7 @@ import { useQueryState, useQueryStates, } from 'nuqs'; -import { UseControllerProps, useForm } from 'react-hook-form'; +import { UseControllerProps, useForm, useWatch } from 'react-hook-form'; import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata'; import { DEFAULT_AUTO_GRANULARITY_MAX_BUCKETS } from '@hyperdx/common-utils/dist/core/renderChartConfig'; import { @@ -128,7 +128,7 @@ function ServiceSelectControlled({ const { expressions } = useServiceDashboardExpressions({ source }); const queriedConfig = { - ...source, + timestampValueExpression: source?.timestampValueExpression || '', from: { databaseName: source?.from.databaseName || '', tableName: source?.from.tableName || '', @@ -240,7 +240,11 @@ export function EndpointLatencyChart({ 'avg_duration_ns', ]} config={{ - ...source, + ...pick(source, [ + 'timestampValueExpression', + 'connection', + 'from', + ]), where: appliedConfig.where || '', whereLanguage: appliedConfig.whereLanguage || 'sql', select: [ @@ -289,7 +293,11 @@ export function EndpointLatencyChart({ ) : ( { + if (!sources?.length) return appliedConfigParams; + + const traceSources = sources?.filter(s => s.kind === SourceKind.Trace); + const paramsSourceIdIsTraceSource = traceSources?.find( + s => s.id === appliedConfigParams.source, + ); + + const effectiveSourceId = paramsSourceIdIsTraceSource + ? appliedConfigParams.source + : traceSources?.[0]?.id || ''; + + return { + ...appliedConfigParams, + source: effectiveSourceId, + }; + }, [appliedConfigParams, sources]); + const { control, watch, setValue, handleSubmit } = useForm({ - values: { + defaultValues: { where: '', whereLanguage: 'sql' as 'sql' | 'lucene', service: appliedConfig?.service || '', - source: appliedConfig?.source || sources?.[0]?.id, + source: appliedConfig?.source ?? '', }, }); @@ -1357,11 +1407,19 @@ function ServicesDashboardPage() { id: watch('source'), }); + // Update the `source` query parameter if the appliedConfig source changes useEffect(() => { - if (sourceId && !appliedConfig.source) { - setAppliedConfig({ source: sourceId }); + if ( + appliedConfig.source && + appliedConfig.source !== appliedConfigParams.source + ) { + setAppliedConfigParams({ source: appliedConfig.source }); } - }, [appliedConfig.source, setAppliedConfig, sourceId]); + }, [ + appliedConfig.source, + appliedConfigParams.source, + setAppliedConfigParams, + ]); const DEFAULT_INTERVAL = 'Past 1h'; const [displayedTimeInputValue, setDisplayedTimeInputValue] = @@ -1374,7 +1432,7 @@ function ServicesDashboardPage() { }); // For future use if Live button is added - const [isLive, setIsLive] = useState(false); + const [isLive, _setIsLive] = useState(false); const { manualRefreshCooloff, refresh } = useDashboardRefresh({ searchedTimeRange, @@ -1385,30 +1443,38 @@ function ServicesDashboardPage() { const onSubmit = useCallback(() => { onSearch(displayedTimeInputValue); handleSubmit(values => { - setAppliedConfig(values); + setAppliedConfigParams(values); })(); - }, [handleSubmit, setAppliedConfig, onSearch, displayedTimeInputValue]); + }, [handleSubmit, setAppliedConfigParams, onSearch, displayedTimeInputValue]); - // Auto submit when service or source changes + // Auto-submit when source changes useEffect(() => { - const normalizedService = service ?? ''; - const appliedService = appliedConfig.service ?? ''; - const normalizedSource = sourceId ?? ''; - const appliedSource = appliedConfig.source ?? ''; + const { unsubscribe } = watch((data, { name, type }) => { + if ( + name === 'source' && + type === 'change' && + data.source && + data.source !== appliedConfig.source + ) { + onSubmit(); + } + }); + return () => unsubscribe(); + }, [appliedConfig.source, onSubmit, watch]); - if ( - normalizedService !== appliedService || - (normalizedSource && normalizedSource !== appliedSource) - ) { - onSubmit(); - } - }, [ - service, - sourceId, - appliedConfig.service, - appliedConfig.source, - onSubmit, - ]); + // Auto-submit when service changes + useEffect(() => { + const { unsubscribe } = watch((data, { name, type }) => { + if ( + name === 'service' && + type === 'change' && + data.service !== appliedConfig.service + ) { + onSubmit(); + } + }); + return () => unsubscribe(); + }, [appliedConfig.service, onSubmit, watch]); return ( @@ -1554,7 +1620,7 @@ const ServicesDashboardPageDynamic = dynamic( }, ); -// @ts-ignore +// @ts-expect-error Next.js layout typing ServicesDashboardPageDynamic.getLayout = withAppNav; export default ServicesDashboardPageDynamic; diff --git a/packages/app/src/components/ServiceDashboardDbQuerySidePanel.tsx b/packages/app/src/components/ServiceDashboardDbQuerySidePanel.tsx index 35a7739442..7e7bf49c37 100644 --- a/packages/app/src/components/ServiceDashboardDbQuerySidePanel.tsx +++ b/packages/app/src/components/ServiceDashboardDbQuerySidePanel.tsx @@ -1,4 +1,5 @@ import { useCallback, useMemo } from 'react'; +import { pick } from 'lodash'; import { parseAsString, useQueryState } from 'nuqs'; import type { Filter } from '@hyperdx/common-utils/dist/types'; import { Drawer, Grid, Group, Text } from '@mantine/core'; @@ -97,7 +98,11 @@ export default function ServiceDashboardDbQuerySidePanel({ sourceId={sourceId} hiddenSeries={['total_duration_ns']} config={{ - ...source, + ...pick(source, [ + 'timestampValueExpression', + 'connection', + 'from', + ]), where: '', whereLanguage: 'sql', select: [ @@ -130,7 +135,11 @@ export default function ServiceDashboardDbQuerySidePanel({ | undefined; try { - // This will likely error for a CTE - materializedFields = chartConfig.with?.length - ? undefined - : await metadata.getMaterializedColumnsLookupTable({ - connectionId: chartConfig.connection, - databaseName: chartConfig.from.databaseName, - tableName: chartConfig.from.tableName, - }); + // This will likely error when referencing a CTE, which is assumed + // to be the case when chartConfig.from.databaseName is not set. + materializedFields = + chartConfig.with?.length || !chartConfig.from.databaseName + ? undefined + : await metadata.getMaterializedColumnsLookupTable({ + connectionId: chartConfig.connection, + databaseName: chartConfig.from.databaseName, + tableName: chartConfig.from.tableName, + }); } catch { // ignore } @@ -689,14 +691,16 @@ async function renderWhereExpression({ let materializedFields: Map | undefined; try { - // This will likely error for a CTE - materializedFields = withClauses?.length - ? undefined - : await metadata.getMaterializedColumnsLookupTable({ - connectionId, - databaseName: from.databaseName, - tableName: from.tableName, - }); + // This will likely error when referencing a CTE, which is assumed + // to be the case when from.databaseName is not set. + materializedFields = + withClauses?.length || !from.databaseName + ? undefined + : await metadata.getMaterializedColumnsLookupTable({ + connectionId, + databaseName: from.databaseName, + tableName: from.tableName, + }); } catch { // ignore }