Fix autocomplete for (#713)

Added an override to provide fields as an input to SearchInputV2 (for lucene) and SqlInlineEditorControlled (for sql), instead of fetching in those respective components. This fixes autocomplete for fields on DBDashboardPage, which was not working previously. I have a fix for field values coming in a future PR.

![image](https://github.com/user-attachments/assets/93de4345-7e8d-4157-aaa1-d223ec49fc72)

![image](https://github.com/user-attachments/assets/c73eb00d-da0e-4fc3-adb4-3f9d2c600a70)

Ref: HDX-1471
This commit is contained in:
Aaron Knudtson 2025-03-27 15:17:29 -04:00 committed by GitHub
parent e002c2f9c6
commit b99236d774
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 421 additions and 242 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
fix: autocomplete options for dashboard page

View file

@ -17,6 +17,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import RGL, { WidthProvider } from 'react-grid-layout';
import { Controller, useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { TableConnection } from '@hyperdx/common-utils/dist/metadata';
import { AlertState } from '@hyperdx/common-utils/dist/types';
import {
ChartConfigWithDateRange,
@ -68,6 +69,7 @@ import DBRowSidePanel from './components/DBRowSidePanel';
import OnboardingModal from './components/OnboardingModal';
import { Tags } from './components/Tags';
import { useDashboardRefresh } from './hooks/useDashboardRefresh';
import { useAllFields } from './hooks/useMetadata';
import { DEFAULT_CHART_CONFIG } from './ChartUtils';
import { IS_LOCAL_MODE } from './config';
import { useDashboard } from './dashboard';
@ -506,6 +508,28 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
const { data: sources } = useSources();
const [highlightedTileId] = useQueryState('highlightedTileId');
const tableConnections = useMemo(() => {
if (!dashboard) return [];
const tc: TableConnection[] = [];
for (const { config } of dashboard.tiles) {
const source = sources?.find(v => v.id === config.source);
if (!source) continue;
// TODO: will need to update this when we allow for multiple metrics per chart
const firstSelect = config.select[0];
const metricType =
typeof firstSelect !== 'string' ? firstSelect?.metricType : undefined;
const tableName = getMetricTableName(source, metricType);
if (!tableName) continue;
tc.push({
databaseName: source.from.databaseName,
tableName: tableName,
connectionId: source.connection,
});
}
return tc;
}, [dashboard, sources]);
const [granularity, setGranularity] = useQueryState(
'granularity',
@ -686,13 +710,6 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
],
);
const uniqueSources = useMemo(() => {
return [...new Set(dashboard?.tiles.map(tile => tile.config.source))];
}, [dashboard?.tiles]);
const { data: defaultSource } = useSource({ id: uniqueSources[0] });
const defaultDatabaseName = defaultSource?.from.databaseName;
const defaultTableName = defaultSource?.from.tableName;
const deleteDashboard = useDeleteDashboard();
// Search tile
@ -886,9 +903,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
render={({ field }) =>
field.value === 'sql' ? (
<SQLInlineEditorControlled
connectionId={defaultSource?.connection}
database={defaultDatabaseName}
table={defaultTableName}
tableConnections={tableConnections}
control={control}
name="where"
placeholder="SQL WHERE clause (ex. column = 'foo')"
@ -900,9 +915,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
/>
) : (
<SearchInputV2
connectionId={defaultSource?.connection}
database={defaultDatabaseName}
table={defaultTableName}
tableConnections={tableConnections}
control={control}
name="where"
onLanguageChange={lang => setValue('whereLanguage', lang)}

View file

@ -20,6 +20,7 @@ import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import {
ChartConfigWithDateRange,
DisplayType,
@ -564,8 +565,6 @@ function DBSearchPage() {
// const { data: inputSourceObj } = useSource({ id: inputSource });
const { data: inputSourceObjs } = useSources();
const inputSourceObj = inputSourceObjs?.find(s => s.id === inputSource);
const databaseName = inputSourceObj?.from.databaseName;
const tableName = inputSourceObj?.from.tableName;
// When source changes, make sure select and orderby fields are set to default
const defaultOrderBy = useMemo(
@ -992,9 +991,7 @@ function DBSearchPage() {
</Group>
<Box style={{ minWidth: 100, flexGrow: 1 }}>
<SQLInlineEditorControlled
connectionId={inputSourceObj?.connection}
database={databaseName}
table={tableName}
tableConnections={tcFromSource(inputSourceObj)}
control={control}
name="select"
defaultValue={inputSourceObj?.defaultTableSelectExpression}
@ -1008,9 +1005,7 @@ function DBSearchPage() {
</Box>
<Box style={{ maxWidth: 400, width: '20%' }}>
<SQLInlineEditorControlled
connectionId={inputSourceObj?.connection}
database={databaseName}
table={tableName}
tableConnections={tcFromSource(inputSourceObj)}
control={control}
name="orderBy"
defaultValue={defaultOrderBy}
@ -1127,9 +1122,7 @@ function DBSearchPage() {
control={control}
sqlInput={
<SQLInlineEditorControlled
connectionId={inputSourceObj?.connection}
database={databaseName}
table={tableName}
tableConnections={tcFromSource(inputSourceObj)}
control={control}
name="where"
placeholder="SQL WHERE clause (ex. column = 'foo')"
@ -1146,9 +1139,7 @@ function DBSearchPage() {
}
luceneInput={
<SearchInputV2
connectionId={inputSourceObj?.connection}
database={databaseName}
table={tableName}
tableConnections={tcFromSource(inputSourceObj)}
control={control}
name="where"
onLanguageChange={lang =>

View file

@ -4,6 +4,7 @@ import { useForm } from 'react-hook-form';
import { NativeSelect, NumberInput } from 'react-hook-form-mantine';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import {
type Alert,
AlertIntervalSchema,
@ -83,10 +84,6 @@ const AlertForm = ({
}) => {
const { data: source } = useSource({ id: sourceId });
const databaseName = source?.from.databaseName;
const tableName = source?.from.tableName;
const connectionId = source?.connection;
const { control, handleSubmit, watch } = useForm<Alert>({
defaultValues: defaultValues || {
interval: '5m',
@ -148,10 +145,8 @@ const AlertForm = ({
grouped by
</Text>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={tcFromSource(source)}
control={control}
connectionId={connectionId}
name={`groupBy`}
placeholder="SQL Columns"
disableKeywordAutocomplete

View file

@ -1,30 +1,27 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useController, UseControllerProps } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { TableConnection } from '@hyperdx/common-utils/dist/metadata';
import { genEnglishExplanation } from '@hyperdx/common-utils/dist/queryParser';
import AutocompleteInput from '@/AutocompleteInput';
import { useAllFields } from '@/hooks/useMetadata';
export default function SearchInputV2({
database,
tableConnections,
placeholder = 'Search your events for anything...',
size = 'sm',
table,
zIndex,
language,
onLanguageChange,
connectionId,
enableHotkey,
onSubmit,
additionalSuggestions,
...props
}: {
database?: string;
tableConnections?: TableConnection | TableConnection[];
placeholder?: string;
size?: 'xs' | 'sm' | 'lg';
table?: string;
connectionId: string | undefined;
zIndex?: number;
onLanguageChange?: (language: 'sql' | 'lucene') => void;
language?: 'sql' | 'lucene';
@ -38,16 +35,11 @@ export default function SearchInputV2({
const ref = useRef<HTMLInputElement>(null);
const { data: fields } = useAllFields(
{
databaseName: database ?? '',
tableName: table ?? '',
connectionId: connectionId ?? '',
},
{
enabled: !!database && !!table && !!connectionId,
},
);
const { data: fields } = useAllFields(tableConnections ?? [], {
enabled:
!!tableConnections &&
(Array.isArray(tableConnections) ? tableConnections.length > 0 : true),
});
const autoCompleteOptions = useMemo(() => {
const _columns = (fields ?? []).filter(c => c.jsType !== null);

View file

@ -7,6 +7,7 @@ import {
useQueryStates,
} from 'nuqs';
import { UseControllerProps, useForm } from 'react-hook-form';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import { DisplayType, Filter, TSource } from '@hyperdx/common-utils/dist/types';
import {
Box,
@ -915,9 +916,7 @@ function ServicesDashboardPage() {
control={control}
sqlInput={
<SQLInlineEditorControlled
connectionId={source?.connection}
database={source?.from?.databaseName}
table={source?.from?.tableName}
tableConnections={tcFromSource(source)}
onSubmit={onSubmit}
control={control}
name="where"
@ -934,9 +933,7 @@ function ServicesDashboardPage() {
}
luceneInput={
<SearchInputV2
connectionId={source?.connection}
database={source?.from?.databaseName}
table={source?.from?.tableName}
tableConnections={tcFromSource(source)}
control={control}
name="where"
onLanguageChange={lang =>

View file

@ -4,6 +4,7 @@ import throttle from 'lodash/throttle';
import { parseAsInteger, useQueryState } from 'nuqs';
import ReactDOM from 'react-dom';
import { useForm } from 'react-hook-form';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import {
ChartConfigWithOptDateRange,
DateRange,
@ -488,9 +489,7 @@ export default function SessionSubpanel({
>
{whereLanguage === 'sql' ? (
<SQLInlineEditorControlled
connectionId={traceSource?.connection}
database={traceSource?.from?.databaseName}
table={traceSource?.from?.tableName}
tableConnections={tcFromSource(traceSource)}
control={control}
name="where"
placeholder="SQL WHERE clause (ex. column = 'foo')"
@ -500,9 +499,7 @@ export default function SessionSubpanel({
/>
) : (
<SearchInputV2
connectionId={traceSource?.connection}
database={traceSource?.from?.databaseName}
table={traceSource?.from?.tableName}
tableConnections={tcFromSource(traceSource)}
control={control}
name="where"
language="lucene"

View file

@ -15,6 +15,7 @@ import {
useQueryParams,
withDefault,
} from 'use-query-params';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import {
DateRange,
SearchCondition,
@ -436,9 +437,7 @@ export default function SessionsPage() {
control={control}
sqlInput={
<SQLInlineEditorControlled
connectionId={traceTrace?.connection}
database={traceTrace?.from?.databaseName}
table={traceTrace?.from?.tableName}
tableConnections={tcFromSource(traceTrace)}
onSubmit={onSubmit}
control={control}
name="where"
@ -455,9 +454,7 @@ export default function SessionsPage() {
}
luceneInput={
<SearchInputV2
connectionId={traceTrace?.connection}
database={traceTrace?.from?.databaseName}
table={traceTrace?.from?.tableName}
tableConnections={tcFromSource(traceTrace)}
control={control}
name="where"
onLanguageChange={lang =>

View file

@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
import { sq } from 'date-fns/locale';
import ms from 'ms';
import { useForm } from 'react-hook-form';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import {
ChartConfigWithDateRange,
TSource,
@ -169,9 +170,7 @@ export default function ContextSubpanel({
sqlInput={
originalLanguage === 'lucene' ? null : (
<SQLInlineEditorControlled
connectionId={source.connection}
database={source.from.databaseName}
table={source.from.tableName}
tableConnections={tcFromSource(source)}
control={control}
name="where"
placeholder="SQL WHERE clause (ex. column = 'foo')"
@ -184,9 +183,7 @@ export default function ContextSubpanel({
luceneInput={
originalLanguage === 'sql' ? null : (
<SearchInputV2
connectionId={source.connection}
database={source.from.databaseName}
table={source.from.tableName}
tableConnections={tcFromSource(source)}
control={control}
name="where"
language="lucene"

View file

@ -10,6 +10,7 @@ import {
import { NativeSelect, NumberInput } from 'react-hook-form-mantine';
import z from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import {
AlertBaseSchema,
ChartConfigWithDateRange,
@ -200,11 +201,13 @@ function ChartSeriesEditor({
{tableSource?.kind !== SourceKind.Metric && aggFn !== 'count' && (
<div style={{ minWidth: 220 }}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName: tableName ?? '',
connectionId: connectionId ?? '',
}}
control={control}
name={`${namePrefix}valueExpression`}
connectionId={connectionId}
placeholder="SQL Column"
onSubmit={onSubmit}
/>
@ -213,9 +216,11 @@ function ChartSeriesEditor({
<Text size="sm">Where</Text>
{aggConditionLanguage === 'sql' ? (
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
connectionId={connectionId}
tableConnections={{
databaseName,
tableName: tableName ?? '',
connectionId: connectionId ?? '',
}}
control={control}
name={`${namePrefix}aggCondition`}
placeholder="SQL WHERE clause (ex. column = 'foo')"
@ -228,9 +233,11 @@ function ChartSeriesEditor({
/>
) : (
<SearchInputV2
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
connectionId: connectionId ?? '',
databaseName: databaseName ?? '',
tableName: tableName ?? '',
}}
control={control}
name={`${namePrefix}aggCondition`}
onLanguageChange={lang =>
@ -249,10 +256,12 @@ function ChartSeriesEditor({
</Text>
<div style={{ minWidth: 300 }}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName: tableName ?? '',
connectionId: connectionId ?? '',
}}
control={control}
connectionId={connectionId}
name={`groupBy`}
placeholder="SQL Columns"
disableKeywordAutocomplete
@ -586,9 +595,7 @@ export default function EditTimeChartForm({
</Text>
<div style={{ flexGrow: 1 }}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
connectionId={tableSource?.connection}
tableConnections={tcFromSource(tableSource)}
control={control}
name={`groupBy`}
placeholder="SQL Columns"
@ -648,9 +655,7 @@ export default function EditTimeChartForm({
) : (
<Flex gap="xs" direction="column">
<SQLInlineEditorControlled
connectionId={tableSource?.connection}
database={databaseName}
table={tableName}
tableConnections={tcFromSource(tableSource)}
control={control}
name="select"
placeholder={
@ -662,9 +667,7 @@ export default function EditTimeChartForm({
/>
{whereLanguage === 'sql' ? (
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
connectionId={tableSource?.connection}
tableConnections={tcFromSource(tableSource)}
control={control}
name={`where`}
placeholder="SQL WHERE clause (ex. column = 'foo')"
@ -674,9 +677,11 @@ export default function EditTimeChartForm({
/>
) : (
<SearchInputV2
connectionId={tableSource?.connection}
database={databaseName}
table={tableName}
tableConnections={{
connectionId: tableSource?.connection ?? '',
databaseName: databaseName ?? '',
tableName: tableName ?? '',
}}
control={control}
name="where"
onLanguageChange={lang => setValue('whereLanguage', lang)}

View file

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { parseAsJson, useQueryState } from 'nuqs';
import { useForm } from 'react-hook-form';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import { SourceKind } from '@hyperdx/common-utils/dist/types';
import {
ActionIcon,
@ -176,9 +177,7 @@ export default function DBTracePanel({
</Text>
<Flex>
<SQLInlineEditorControlled
connectionId={parentSourceData?.id}
database={parentSourceData?.from.databaseName}
table={parentSourceData?.from.tableName}
tableConnections={tcFromSource(parentSourceData)}
name="traceIdExpression"
placeholder="Log Trace ID Column (ex. trace_id)"
control={traceIdControl}

View file

@ -3,7 +3,7 @@ import { useController, UseControllerProps } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { acceptCompletion, startCompletion } from '@codemirror/autocomplete';
import { sql, SQLDialect } from '@codemirror/lang-sql';
import { Field } from '@hyperdx/common-utils/dist/metadata';
import { Field, TableConnection } from '@hyperdx/common-utils/dist/metadata';
import { Paper, Text } from '@mantine/core';
import CodeMirror, {
Compartment,
@ -89,8 +89,8 @@ const AUTOCOMPLETE_LIST_FOR_SQL_FUNCTIONS = [
const AUTOCOMPLETE_LIST_STRING = ` ${AUTOCOMPLETE_LIST_FOR_SQL_FUNCTIONS.join(' ')}`;
type SQLInlineEditorProps = {
database?: string | undefined;
table?: string | undefined;
tableConnections?: TableConnection | TableConnection[];
autoCompleteFields?: Field[];
filterField?: (field: Field) => boolean;
value: string;
onChange: (value: string) => void;
@ -101,7 +101,6 @@ type SQLInlineEditorProps = {
size?: string;
label?: React.ReactNode;
disableKeywordAutocomplete?: boolean;
connectionId: string | undefined;
enableHotkey?: boolean;
additionalSuggestions?: string[];
};
@ -119,33 +118,25 @@ const styleTheme = EditorView.baseTheme({
});
export default function SQLInlineEditor({
database,
tableConnections,
filterField,
onChange,
placeholder,
onLanguageChange,
language,
onSubmit,
table,
value,
size,
label,
disableKeywordAutocomplete,
connectionId,
enableHotkey,
additionalSuggestions = [],
}: SQLInlineEditorProps) {
const { data: fields } = useAllFields(
{
databaseName: database ?? '',
tableName: table ?? '',
connectionId: connectionId ?? '',
},
{
enabled: !!database && !!table && !!connectionId,
},
);
const { data: fields } = useAllFields(tableConnections ?? [], {
enabled:
!!tableConnections &&
(Array.isArray(tableConnections) ? tableConnections.length > 0 : true),
});
const filteredFields = useMemo(() => {
return filterField ? fields?.filter(filterField) : fields;
}, [fields, filterField]);
@ -171,7 +162,6 @@ export default function SQLInlineEditor({
viewRef.dispatch({
effects: compartmentRef.current.reconfigure(
sql({
defaultTable: table ?? '',
dialect: SQLDialect.define({
keywords:
keywords.join(' ') +
@ -181,7 +171,7 @@ export default function SQLInlineEditor({
),
});
},
[filteredFields, table, additionalSuggestions],
[filteredFields, additionalSuggestions],
);
useEffect(() => {
@ -292,11 +282,8 @@ export default function SQLInlineEditor({
}
export function SQLInlineEditorControlled({
database,
table,
placeholder,
filterField,
connectionId,
additionalSuggestions,
...props
}: Omit<SQLInlineEditorProps, 'value' | 'onChange'> & UseControllerProps<any>) {
@ -304,13 +291,10 @@ export function SQLInlineEditorControlled({
return (
<SQLInlineEditor
database={database}
filterField={filterField}
onChange={field.onChange}
placeholder={placeholder}
table={table}
value={field.value || props.defaultValue}
connectionId={connectionId}
additionalSuggestions={additionalSuggestions}
{...props}
/>

View file

@ -159,12 +159,14 @@ export function LogTableModelForm({
helpText="DateTime column or expression that is part of your table's primary key."
>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="timestampValueExpression"
disableKeywordAutocomplete
connectionId={connectionId}
/>
</FormRow>
<FormRow
@ -172,12 +174,14 @@ export function LogTableModelForm({
helpText="Default columns selected in search results (this can be customized per search later)"
>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="defaultTableSelectExpression"
placeholder="Timestamp, Body"
connectionId={connectionId}
/>
</FormRow>
<Box>
@ -215,52 +219,62 @@ export function LogTableModelForm({
<Divider />
<FormRow label={'Service Name Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="serviceNameExpression"
placeholder="ServiceName"
connectionId={connectionId}
/>
</FormRow>
<FormRow label={'Log Level Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="severityTextExpression"
placeholder="SeverityText"
connectionId={connectionId}
/>
</FormRow>
<FormRow label={'Body Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="bodyExpression"
placeholder="Body"
connectionId={connectionId}
/>
</FormRow>
<FormRow label={'Log Attributes Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="eventAttributesExpression"
placeholder="LogAttributes"
connectionId={connectionId}
/>
</FormRow>
<FormRow label={'Resource Attributes Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="resourceAttributesExpression"
placeholder="ResourceAttributes"
connectionId={connectionId}
/>
</FormRow>
<FormRow
@ -268,12 +282,14 @@ export function LogTableModelForm({
helpText="This DateTime column is used to display search results."
>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="displayedTimestampValueExpression"
disableKeywordAutocomplete
connectionId={connectionId}
/>
</FormRow>
<Divider />
@ -292,22 +308,26 @@ export function LogTableModelForm({
<FormRow label={'Trace Id Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="traceIdExpression"
placeholder="TraceId"
connectionId={connectionId}
/>
</FormRow>
<FormRow label={'Span Id Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="spanIdExpression"
placeholder="SpanId"
connectionId={connectionId}
/>
</FormRow>
@ -317,22 +337,26 @@ export function LogTableModelForm({
helpText="Unique identifier for a given row, will be primary key if not specified. Used for showing full row details in search results."
>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="uniqueRowIdExpression"
placeholder="Timestamp, ServiceName, Body"
connectionId={connectionId}
/>
</FormRow> */}
{/* <FormRow label={'Table Filter Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="tableFilterExpression"
placeholder="ServiceName = 'only_this_service'"
connectionId={connectionId}
/>
</FormRow> */}
<FormRow
@ -340,12 +364,14 @@ export function LogTableModelForm({
helpText="Column used for full text search if no property is specified in a Lucene-based search. Typically the message body of a log."
>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="implicitColumnExpression"
placeholder="Body"
connectionId={connectionId}
/>
</FormRow>
</Stack>
@ -389,9 +415,11 @@ export function TraceTableModelForm({
</FormRow>
<FormRow label={'Timestamp Column'}>
<SQLInlineEditorControlled
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="timestampValueExpression"
placeholder="Timestamp"
@ -403,20 +431,24 @@ export function TraceTableModelForm({
helpText="Default columns selected in search results (this can be customized per search later)"
>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="defaultTableSelectExpression"
placeholder="Timestamp, ServiceName, StatusCode, Duration, SpanName"
connectionId={connectionId}
/>
</FormRow>
<Divider />
<FormRow label={'Duration Expression'}>
<SQLInlineEditorControlled
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="durationExpression"
placeholder="Duration Column"
@ -450,9 +482,11 @@ export function TraceTableModelForm({
</FormRow>
<FormRow label={'Trace Id Expression'}>
<SQLInlineEditorControlled
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="traceIdExpression"
placeholder="TraceId"
@ -460,9 +494,11 @@ export function TraceTableModelForm({
</FormRow>
<FormRow label={'Span Id Expression'}>
<SQLInlineEditorControlled
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="spanIdExpression"
placeholder="SpanId"
@ -470,9 +506,11 @@ export function TraceTableModelForm({
</FormRow>
<FormRow label={'Parent Span Id Expression'}>
<SQLInlineEditorControlled
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="parentSpanIdExpression"
placeholder="ParentSpanId"
@ -480,9 +518,11 @@ export function TraceTableModelForm({
</FormRow>
<FormRow label={'Span Name Expression'}>
<SQLInlineEditorControlled
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="spanNameExpression"
placeholder="SpanName"
@ -490,9 +530,11 @@ export function TraceTableModelForm({
</FormRow>
<FormRow label={'Span Kind Expression'}>
<SQLInlineEditorControlled
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="spanKindExpression"
placeholder="SpanKind"
@ -513,9 +555,11 @@ export function TraceTableModelForm({
</FormRow>
<FormRow label={'Status Code Expression'}>
<SQLInlineEditorControlled
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="statusCodeExpression"
placeholder="StatusCode"
@ -523,9 +567,11 @@ export function TraceTableModelForm({
</FormRow>
<FormRow label={'Status Message Expression'}>
<SQLInlineEditorControlled
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="statusMessageExpression"
placeholder="StatusMessage"
@ -533,9 +579,11 @@ export function TraceTableModelForm({
</FormRow>
<FormRow label={'Service Name Expression'}>
<SQLInlineEditorControlled
connectionId={connectionId}
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="serviceNameExpression"
placeholder="ServiceName"
@ -543,22 +591,26 @@ export function TraceTableModelForm({
</FormRow>
<FormRow label={'Resource Attributes Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="resourceAttributesExpression"
placeholder="ResourceAttributes"
connectionId={connectionId}
/>
</FormRow>
<FormRow label={'Event Attributes Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="eventAttributesExpression"
placeholder="SpanAttributes"
connectionId={connectionId}
/>
</FormRow>
<FormRow
@ -566,12 +618,14 @@ export function TraceTableModelForm({
helpText="Column used for full text search if no property is specified in a Lucene-based search. Typically the message body of a log."
>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="implicitColumnExpression"
placeholder="SpanName"
connectionId={connectionId}
/>
</FormRow>
</Stack>
@ -620,32 +674,38 @@ export function SessionTableModelForm({
helpText="DateTime column or expression that is part of your table's primary key."
>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="timestampValueExpression"
disableKeywordAutocomplete
connectionId={connectionId}
/>
</FormRow>
<FormRow label={'Log Attributes Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="eventAttributesExpression"
placeholder="LogAttributes"
connectionId={connectionId}
/>
</FormRow>
<FormRow label={'Resource Attributes Expression'}>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="resourceAttributesExpression"
placeholder="ResourceAttributes"
connectionId={connectionId}
/>
</FormRow>
<FormRow
@ -659,12 +719,14 @@ export function SessionTableModelForm({
helpText="Column used for full text search if no property is specified in a Lucene-based search. Typically the message body of a log."
>
<SQLInlineEditorControlled
database={databaseName}
table={tableName}
tableConnections={{
databaseName,
tableName,
connectionId,
}}
control={control}
name="implicitColumnExpression"
placeholder="Body"
connectionId={connectionId}
/>
</FormRow>
</Stack>

View file

@ -0,0 +1,94 @@
import { deduplicate2dArray } from '../useMetadata';
describe('deduplicate2dArray', () => {
// Test basic deduplication
it('should remove duplicate objects across 2D array', () => {
const input = [
[
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
],
[
{ id: 1, name: 'Alice' },
{ id: 3, name: 'Charlie' },
],
];
const result = deduplicate2dArray(input);
expect(result).toHaveLength(3);
expect(result).toEqual([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' },
]);
});
// Test with empty arrays
it('should handle empty 2D array', () => {
const input: object[][] = [];
const result = deduplicate2dArray(input);
expect(result).toHaveLength(0);
});
// Test with nested empty arrays
it('should handle 2D array with empty subarrays', () => {
const input = [[], [], []];
const result = deduplicate2dArray(input);
expect(result).toHaveLength(0);
});
// Test with complex objects
it('should deduplicate complex nested objects', () => {
const input = [
[
{ user: { id: 1, details: { name: 'Alice' } } },
{ user: { id: 2, details: { name: 'Bob' } } },
],
[
{ user: { id: 1, details: { name: 'Alice' } } },
{ user: { id: 3, details: { name: 'Charlie' } } },
],
];
const result = deduplicate2dArray(input);
expect(result).toHaveLength(3);
expect(result).toEqual([
{ user: { id: 1, details: { name: 'Alice' } } },
{ user: { id: 2, details: { name: 'Bob' } } },
{ user: { id: 3, details: { name: 'Charlie' } } },
]);
});
// Test with different types of objects
it('should work with different types of objects', () => {
const input: {
value: any;
}[][] = [
[{ value: 'string' }, { value: 42 }],
[{ value: 'string' }, { value: true }],
];
const result = deduplicate2dArray(input);
expect(result).toHaveLength(3);
});
// Test order preservation
it('should preserve the order of first occurrence', () => {
const input = [
[{ id: 1 }, { id: 2 }],
[{ id: 1 }, { id: 3 }],
[{ id: 4 }, { id: 2 }],
];
const result = deduplicate2dArray(input);
expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]);
});
});

View file

@ -1,5 +1,11 @@
import objectHash from 'object-hash';
import { ColumnMeta } from '@hyperdx/common-utils/dist/clickhouse';
import { Field, TableMetadata } from '@hyperdx/common-utils/dist/metadata';
import {
Field,
isSingleTableConnection,
TableConnection,
TableMetadata,
} from '@hyperdx/common-utils/dist/metadata';
import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types';
import {
keepPreviousData,
@ -36,26 +42,27 @@ export function useColumns(
}
export function useAllFields(
{
databaseName,
tableName,
connectionId,
}: {
databaseName: string;
tableName: string;
connectionId: string;
},
_tableConnections: TableConnection | TableConnection[],
options?: Partial<UseQueryOptions<Field[]>>,
) {
const tableConnections = isSingleTableConnection(_tableConnections)
? [_tableConnections]
: _tableConnections;
const metadata = getMetadata();
return useQuery<Field[]>({
queryKey: ['useMetadata.useAllFields', { databaseName, tableName }],
queryKey: [
'useMetadata.useAllFields',
...tableConnections.map(tc => ({ ...tc })),
],
queryFn: async () => {
return metadata.getAllFields({
databaseName,
tableName,
connectionId,
});
const fields2d = await Promise.all(
tableConnections.map(tc => metadata.getAllFields(tc)),
);
// skip deduplication if not needed
if (fields2d.length === 1) return fields2d[0];
return deduplicate2dArray<Field>(fields2d);
},
...options,
});
@ -115,3 +122,18 @@ export function useGetKeyValues({
placeholderData: keepPreviousData,
});
}
export function deduplicate2dArray<T extends object>(array2d: T[][]): T[] {
// deduplicate common fields
const array: T[] = [];
const set = new Set<string>();
for (const _array of array2d) {
for (const elem of _array) {
const key = objectHash.sha1(elem);
if (set.has(key)) continue;
set.add(key);
array.push(elem);
}
}
return array;
}

View file

@ -9,7 +9,7 @@ import {
tableExpr,
} from '@/clickhouse';
import { renderChartConfig } from '@/renderChartConfig';
import type { ChartConfigWithDateRange } from '@/types';
import type { ChartConfig, ChartConfigWithDateRange, TSource } from '@/types';
const DEFAULT_SAMPLE_SIZE = 1e6;
@ -353,11 +353,7 @@ export class Metadata {
databaseName,
tableName,
connectionId,
}: {
databaseName: string;
tableName: string;
connectionId: string;
}) {
}: TableConnection) {
const fields: Field[] = [];
const columns = await this.getColumns({
databaseName,
@ -467,6 +463,39 @@ export type Field = {
jsType: JSDataType | null;
};
export type TableConnection = {
databaseName: string;
tableName: string;
connectionId: string;
};
export function isSingleTableConnection(
obj: TableConnection | TableConnection[],
): obj is TableConnection {
return (
!Array.isArray(obj) &&
typeof obj?.databaseName === 'string' &&
typeof obj?.tableName === 'string' &&
typeof obj?.connectionId === 'string'
);
}
export function tcFromChartConfig(config?: ChartConfig): TableConnection {
return {
databaseName: config?.from?.databaseName ?? '',
tableName: config?.from?.tableName ?? '',
connectionId: config?.connection ?? '',
};
}
export function tcFromSource(source?: TSource): TableConnection {
return {
databaseName: source?.from?.databaseName ?? '',
tableName: source?.from?.tableName ?? '',
connectionId: source?.connection ?? '',
};
}
const __LOCAL_CACHE__ = new MetadataCache();
// TODO: better to init the Metadata object on the client side