From 5210bb86a4e5109dd33dba0433ed7ace433498a2 Mon Sep 17 00:00:00 2001 From: Aaron Knudtson <87577305+knudtty@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:33:53 -0400 Subject: [PATCH] refactor: table connections standardized and single by default. Special case for dashboard (#1195) Closes HDX-2469 --- .changeset/two-melons-admire.md | 6 ++ packages/app/src/DBSearchPage.tsx | 8 +-- packages/app/src/DBSearchPageAlertModal.tsx | 2 +- packages/app/src/DashboardFilters.tsx | 2 +- packages/app/src/DashboardFiltersModal.tsx | 2 +- .../app/src/NamespaceDetailsSidePanel.tsx | 2 +- packages/app/src/NodeDetailsSidePanel.tsx | 2 +- packages/app/src/PodDetailsSidePanel.tsx | 2 +- packages/app/src/SearchInputV2.tsx | 14 +++-- packages/app/src/ServicesDashboardPage.tsx | 4 +- packages/app/src/SessionSubpanel.tsx | 4 +- packages/app/src/SessionsPage.tsx | 4 +- .../app/src/components/ContextSidePanel.tsx | 4 +- .../src/components/DBEditTimeChartForm.tsx | 16 ++--- .../src/components/DBSearchPageFilters.tsx | 29 +++------ packages/app/src/components/DBTracePanel.tsx | 2 +- .../app/src/components/KubernetesFilters.tsx | 4 +- .../app/src/components/MetricNameSelect.tsx | 6 +- .../app/src/components/SQLInlineEditor.tsx | 20 ++++-- packages/app/src/components/SourceForm.tsx | 56 ++++++++--------- .../__tests__/useAutoCompleteOptions.test.tsx | 38 +++++------ .../src/hooks/__tests__/useMetadata.test.tsx | 16 +++-- .../app/src/hooks/useAutoCompleteOptions.tsx | 56 +++++++++-------- packages/app/src/hooks/useMetadata.tsx | 63 ++++++++++++++----- packages/common-utils/src/metadata.ts | 10 +++ 25 files changed, 216 insertions(+), 156 deletions(-) create mode 100644 .changeset/two-melons-admire.md diff --git a/.changeset/two-melons-admire.md b/.changeset/two-melons-admire.md new file mode 100644 index 00000000..00282b11 --- /dev/null +++ b/.changeset/two-melons-admire.md @@ -0,0 +1,6 @@ +--- +"@hyperdx/common-utils": patch +"@hyperdx/app": patch +--- + +refactor: clean up table connections diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index e7d9266d..bacc0267 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -1265,7 +1265,7 @@ function DBSearchPage() { diff --git a/packages/app/src/DBSearchPageAlertModal.tsx b/packages/app/src/DBSearchPageAlertModal.tsx index 0d304664..b57f78a8 100644 --- a/packages/app/src/DBSearchPageAlertModal.tsx +++ b/packages/app/src/DBSearchPageAlertModal.tsx @@ -147,7 +147,7 @@ const AlertForm = ({ grouped by ) { +} & UseControllerProps & + TableConnectionChoice) { const { field: { onChange, value }, } = useController(props); @@ -59,9 +63,9 @@ export default function SearchInputV2({ const autoCompleteOptions = useAutoCompleteOptions( luceneLanguageFormatter, - value != null ? `${value}` : '', // value can be null at times + value != null ? `${value}` : '', { - tableConnections, + tableConnection: tableConnection ? tableConnection : tableConnections, additionalSuggestions, }, ); diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index 56e3f87b..9e336e35 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -955,7 +955,7 @@ function ServicesDashboardPage() { control={control} sqlInput={ diff --git a/packages/app/src/SessionSubpanel.tsx b/packages/app/src/SessionSubpanel.tsx index 6fdcef78..2072823a 100644 --- a/packages/app/src/SessionSubpanel.tsx +++ b/packages/app/src/SessionSubpanel.tsx @@ -488,7 +488,7 @@ export default function SessionSubpanel({ > {whereLanguage === 'sql' ? ( ) : ( diff --git a/packages/app/src/components/ContextSidePanel.tsx b/packages/app/src/components/ContextSidePanel.tsx index 21274350..b6e7439c 100644 --- a/packages/app/src/components/ContextSidePanel.tsx +++ b/packages/app/src/components/ContextSidePanel.tsx @@ -260,7 +260,7 @@ export default function ContextSubpanel({ sqlInput={ originalLanguage === 'lucene' ? null : ( Where {aggConditionLanguage === 'sql' ? ( ) : (
{whereLanguage === 'sql' ? ( ) : ( { return _setFilterValue(property, value, action); }; - const { - toggleFilterPin, - toggleFieldPin, - isFilterPinned, - isFieldPinned, - getPinnedFields, - } = usePinnedFilters(sourceId ?? null); + const { toggleFilterPin, toggleFieldPin, isFilterPinned, isFieldPinned } = + usePinnedFilters(sourceId ?? null); const { width, startResize } = useResizable(16, 'left'); - const { data: countData } = useExplainQuery(chartConfig); - const numRows: number = countData?.[0]?.rows ?? 0; - - const { data: jsonColumns } = useJsonColumns({ - databaseName: chartConfig.from.databaseName, - tableName: chartConfig.from.tableName, - connectionId: chartConfig.connection, - }); - const { data, isLoading, error } = useAllFields({ - databaseName: chartConfig.from.databaseName, - tableName: chartConfig.from.tableName, - connectionId: chartConfig.connection, - }); + const { data: jsonColumns } = useJsonColumns(tcFromChartConfig(chartConfig)); + const { data, isLoading, error } = useAllFields( + tcFromChartConfig(chartConfig), + ); const { data: source } = useSource({ id: sourceId }); const { data: tableMetadata } = useTableMetadata(tcFromSource(source)); @@ -647,7 +634,7 @@ const DBSearchPageFiltersComponent = ({ isLoading: isFacetsLoading, isFetching: isFacetsFetching, } = useGetKeyValues({ - chartConfigs: { ...chartConfig, dateRange }, + chartConfig: { ...chartConfig, dateRange }, limit: keyLimit, keys: keysToFetch, }); diff --git a/packages/app/src/components/DBTracePanel.tsx b/packages/app/src/components/DBTracePanel.tsx index db703359..7535df5c 100644 --- a/packages/app/src/components/DBTracePanel.tsx +++ b/packages/app/src/components/DBTracePanel.tsx @@ -156,7 +156,7 @@ export default function DBTracePanel({ = ({ dataTestId, }) => { const { data, isLoading } = useGetKeyValues({ - chartConfigs: chartConfig, + chartConfig, keys: [`${metricSource.resourceAttributesExpression}['${fieldName}']`], }); @@ -240,7 +240,7 @@ export const KubernetesFilters: React.FC = ({ dataTestId="cluster-filter-select" /> boolean; value: string; @@ -143,6 +145,7 @@ const createStyleTheme = (allowMultiline: boolean = false) => }); export default function SQLInlineEditor({ + tableConnection, tableConnections, filterField, onChange, @@ -160,8 +163,11 @@ export default function SQLInlineEditor({ queryHistoryType, parentRef, allowMultiline = false, -}: SQLInlineEditorProps) { - const { data: fields } = useAllFields(tableConnections ?? []); +}: SQLInlineEditorProps & TableConnectionChoice) { + const _tableConnections = tableConnection + ? [tableConnection] + : tableConnections; + const { data: fields } = useMultipleAllFields(_tableConnections ?? []); const filteredFields = useMemo(() => { return filterField ? fields?.filter(filterField) : fields; }, [fields, filterField]); @@ -410,7 +416,9 @@ function SQLInlineEditorControlledComponent({ additionalSuggestions, queryHistoryType, ...props -}: Omit & UseControllerProps) { +}: Omit & + UseControllerProps & + TableConnectionChoice) { const { field, fieldState } = useController(props); // Guard against wrongly typed values diff --git a/packages/app/src/components/SourceForm.tsx b/packages/app/src/components/SourceForm.tsx index c9129b71..98fe39a2 100644 --- a/packages/app/src/components/SourceForm.tsx +++ b/packages/app/src/components/SourceForm.tsx @@ -165,7 +165,7 @@ export function LogTableModelForm({ control, watch }: TableModelProps) { helpText="DateTime column or expression that is part of your table's primary key." > */} {/* { @@ -15,8 +19,8 @@ if (!globalThis.structuredClone) { // Mock dependencies jest.mock('../useMetadata', () => ({ ...jest.requireActual('../useMetadata.tsx'), - useAllFields: jest.fn(), - useGetKeyValues: jest.fn(), + useMultipleAllFields: jest.fn(), + useMultipleGetKeyValues: jest.fn(), useJsonColumns: jest.fn(), })); @@ -40,13 +44,11 @@ const mockFields: Field[] = [ }, ]; -const mockTableConnections = [ - { - databaseName: 'test_db', - tableName: 'traces', - connectionId: 'conn1', - }, -]; +const mockTableConnection = { + databaseName: 'test_db', + tableName: 'traces', + connectionId: 'conn1', +}; describe('useAutoCompleteOptions', () => { beforeEach(() => { @@ -54,11 +56,11 @@ describe('useAutoCompleteOptions', () => { jest.clearAllMocks(); // Setup default mock implementations - (useAllFields as jest.Mock).mockReturnValue({ + (useMultipleAllFields as jest.Mock).mockReturnValue({ data: mockFields, }); - (useGetKeyValues as jest.Mock).mockReturnValue({ + (useMultipleGetKeyValues as jest.Mock).mockReturnValue({ data: null, }); @@ -70,7 +72,7 @@ describe('useAutoCompleteOptions', () => { it('should return field options with correct lucene formatting', () => { const { result } = renderHook(() => useAutoCompleteOptions(luceneFormatter, 'ResourceAttributes', { - tableConnections: mockTableConnections, + tableConnection: mockTableConnection, }), ); @@ -98,7 +100,7 @@ describe('useAutoCompleteOptions', () => { }, ]; - (useGetKeyValues as jest.Mock).mockReturnValue({ + (useMultipleGetKeyValues as jest.Mock).mockReturnValue({ data: mockKeyValues, }); @@ -107,7 +109,7 @@ describe('useAutoCompleteOptions', () => { luceneFormatter, 'ResourceAttributes.service.name', { - tableConnections: mockTableConnections, + tableConnection: mockTableConnection, }, ), ); @@ -150,13 +152,13 @@ describe('useAutoCompleteOptions', () => { }, ]; - (useGetKeyValues as jest.Mock).mockReturnValue({ + (useMultipleGetKeyValues as jest.Mock).mockReturnValue({ data: mockKeyValues, }); const { result } = renderHook(() => useAutoCompleteOptions(luceneFormatter, 'ResourceAttributes', { - tableConnections: mockTableConnections, + tableConnection: mockTableConnection, }), ); @@ -188,7 +190,7 @@ describe('useAutoCompleteOptions', () => { it('should handle additional suggestions', () => { const { result } = renderHook(() => useAutoCompleteOptions(luceneFormatter, 'ResourceAttributes', { - tableConnections: mockTableConnections, + tableConnection: mockTableConnection, additionalSuggestions: ['custom.field'], }), ); diff --git a/packages/app/src/hooks/__tests__/useMetadata.test.tsx b/packages/app/src/hooks/__tests__/useMetadata.test.tsx index 4c1d53a3..ca4c13e2 100644 --- a/packages/app/src/hooks/__tests__/useMetadata.test.tsx +++ b/packages/app/src/hooks/__tests__/useMetadata.test.tsx @@ -6,7 +6,11 @@ import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { renderHook, waitFor } from '@testing-library/react'; -import { deduplicate2dArray, useGetKeyValues } from '../useMetadata'; +import { + deduplicate2dArray, + useGetKeyValues, + useMultipleGetKeyValues, +} from '../useMetadata'; // Create a mock ChartConfig based on the Zod schema const createMockChartConfig = ( @@ -69,7 +73,7 @@ describe('useGetKeyValues', () => { const { result } = renderHook( () => useGetKeyValues({ - chartConfigs: mockChartConfig, + chartConfig: mockChartConfig, keys: mockKeys, }), { wrapper }, @@ -118,7 +122,7 @@ describe('useGetKeyValues', () => { // Act const { result } = renderHook( () => - useGetKeyValues({ + useMultipleGetKeyValues({ chartConfigs: mockChartConfigs, keys: mockKeys, }), @@ -150,7 +154,7 @@ describe('useGetKeyValues', () => { const { result } = renderHook( () => useGetKeyValues({ - chartConfigs: mockChartConfig, + chartConfig: mockChartConfig, keys: [], }), { wrapper }, @@ -180,7 +184,7 @@ describe('useGetKeyValues', () => { const { result } = renderHook( () => useGetKeyValues({ - chartConfigs: mockChartConfig, + chartConfig: mockChartConfig, keys: mockKeys, limit: 50, disableRowLimit: true, @@ -206,7 +210,7 @@ describe('useGetKeyValues', () => { const { result } = renderHook( () => useGetKeyValues({ - chartConfigs: mockChartConfig, + chartConfig: mockChartConfig, keys: mockKeys, }), { wrapper }, diff --git a/packages/app/src/hooks/useAutoCompleteOptions.tsx b/packages/app/src/hooks/useAutoCompleteOptions.tsx index 68d144a1..60eec6b4 100644 --- a/packages/app/src/hooks/useAutoCompleteOptions.tsx +++ b/packages/app/src/hooks/useAutoCompleteOptions.tsx @@ -4,9 +4,9 @@ import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { deduplicate2dArray, - useAllFields, - useGetKeyValues, useJsonColumns, + useMultipleAllFields, + useMultipleGetKeyValues, } from '@/hooks/useMetadata'; import { mergePath, toArray } from '@/utils'; @@ -23,15 +23,21 @@ export function useAutoCompleteOptions( formatter: ILanguageFormatter, value: string, { - tableConnections, + tableConnection, additionalSuggestions, }: { - tableConnections?: TableConnection | TableConnection[]; + tableConnection?: TableConnection | TableConnection[]; additionalSuggestions?: string[]; }, ) { // Fetch and gather all field options - const { data: fields } = useAllFields(tableConnections ?? []); + const { data: fields } = useMultipleAllFields( + tableConnection + ? Array.isArray(tableConnection) + ? tableConnection + : [tableConnection] + : [], + ); const { fieldCompleteOptions, fieldCompleteMap } = useMemo(() => { const _columns = (fields ?? []).filter(c => c.jsType !== null); @@ -72,11 +78,11 @@ export function useAutoCompleteOptions( setSearchField(null); } }, [searchField, setSearchField, value, formatter]); - const tcForJson = Array.isArray(tableConnections) - ? tableConnections.length > 0 - ? tableConnections[0] + const tcForJson = Array.isArray(tableConnection) + ? tableConnection.length > 0 + ? tableConnection[0] : undefined - : tableConnections; + : tableConnection; const { data: jsonColumns } = useJsonColumns( tcForJson ?? { tableName: '', @@ -93,22 +99,22 @@ export function useAutoCompleteOptions( ); // hooks to get key values - const chartConfigs: ChartConfigWithDateRange[] = toArray( - tableConnections, - ).map(({ databaseName, tableName, connectionId }) => ({ - connection: connectionId, - from: { - databaseName, - tableName, - }, - timestampValueExpression: '', - select: '', - where: '', - // TODO: Pull in date for query as arg - // just assuming 1/2 day is okay to query over right now - dateRange: [new Date(NOW - (86400 * 1000) / 2), new Date(NOW)], - })); - const { data: keyVals } = useGetKeyValues({ + const chartConfigs: ChartConfigWithDateRange[] = toArray(tableConnection).map( + ({ databaseName, tableName, connectionId }) => ({ + connection: connectionId, + from: { + databaseName, + tableName, + }, + timestampValueExpression: '', + select: '', + where: '', + // TODO: Pull in date for query as arg + // just assuming 1/2 day is okay to query over right now + dateRange: [new Date(NOW - (86400 * 1000) / 2), new Date(NOW)], + }), + ); + const { data: keyVals } = useMultipleGetKeyValues({ chartConfigs, keys: searchKeys, }); diff --git a/packages/app/src/hooks/useMetadata.tsx b/packages/app/src/hooks/useMetadata.tsx index 506a04b8..baaff678 100644 --- a/packages/app/src/hooks/useMetadata.tsx +++ b/packages/app/src/hooks/useMetadata.tsx @@ -68,41 +68,39 @@ export function useColumns( } export function useJsonColumns( - { databaseName, tableName, connectionId }: TableConnection, + tableConnection: TableConnection | undefined, options?: Partial>, ) { const metadata = useMetadataWithSettings(); return useQuery({ - queryKey: ['useMetadata.useJsonColumns', { databaseName, tableName }], + queryKey: ['useMetadata.useJsonColumns', tableConnection], queryFn: async () => { - const columns = await metadata.getColumns({ - databaseName, - tableName, - connectionId, - }); + if (!tableConnection) return []; + const columns = await metadata.getColumns(tableConnection); return ( filterColumnMetaByType(columns, [JSDataType.JSON])?.map( column => column.name, ) ?? [] ); }, - enabled: !!databaseName && !!tableName && !!connectionId, + enabled: + tableConnection && + !!tableConnection.databaseName && + !!tableConnection.tableName && + !!tableConnection.connectionId, ...options, }); } -export function useAllFields( - _tableConnections: TableConnection | TableConnection[], +export function useMultipleAllFields( + tableConnections: TableConnection[], options?: Partial>, ) { - const tableConnections = Array.isArray(_tableConnections) - ? _tableConnections - : [_tableConnections]; const metadata = useMetadataWithSettings(); const { data: me, isFetched } = api.useMe(); return useQuery({ queryKey: [ - 'useMetadata.useAllFields', + 'useMetadata.useMultipleAllFields', ...tableConnections.map(tc => ({ ...tc })), ], queryFn: async () => { @@ -130,6 +128,16 @@ export function useAllFields( }); } +export function useAllFields( + tableConnection: TableConnection | undefined, + options?: Partial>, +) { + return useMultipleAllFields( + tableConnection ? [tableConnection] : [], + options, + ); +} + export function useTableMetadata( { databaseName, @@ -158,7 +166,7 @@ export function useTableMetadata( }); } -export function useGetKeyValues( +export function useMultipleGetKeyValues( { chartConfigs, keys, @@ -202,6 +210,31 @@ export function useGetKeyValues( }); } +export function useGetKeyValues( + { + chartConfig, + keys, + limit, + disableRowLimit, + }: { + chartConfig?: ChartConfigWithDateRange; + keys: string[]; + limit?: number; + disableRowLimit?: boolean; + }, + options?: Omit, 'queryKey'>, +) { + return useMultipleGetKeyValues( + { + chartConfigs: chartConfig ? [chartConfig] : [], + keys, + limit, + disableRowLimit, + }, + options, + ); +} + export function deduplicateArray(array: T[]): T[] { return deduplicate2dArray([array]); } diff --git a/packages/common-utils/src/metadata.ts b/packages/common-utils/src/metadata.ts index efcb8839..e11f28d8 100644 --- a/packages/common-utils/src/metadata.ts +++ b/packages/common-utils/src/metadata.ts @@ -677,6 +677,16 @@ export type TableConnection = { metricName?: string; }; +export type TableConnectionChoice = + | { + tableConnection?: never; + tableConnections?: TableConnection[]; + } + | { + tableConnection?: TableConnection; + tableConnections?: never; + }; + export function tcFromChartConfig(config?: ChartConfig): TableConnection { return { databaseName: config?.from?.databaseName ?? '',