2026-01-13 12:55:12 +00:00
|
|
|
import { useEffect, useRef, useState } from 'react';
|
2025-03-27 19:17:29 +00:00
|
|
|
import objectHash from 'object-hash';
|
2025-08-08 11:32:24 +00:00
|
|
|
import {
|
|
|
|
|
ColumnMeta,
|
|
|
|
|
filterColumnMetaByType,
|
|
|
|
|
JSDataType,
|
|
|
|
|
} from '@hyperdx/common-utils/dist/clickhouse';
|
2025-03-27 19:17:29 +00:00
|
|
|
import {
|
|
|
|
|
Field,
|
|
|
|
|
TableConnection,
|
|
|
|
|
TableMetadata,
|
2025-10-30 15:16:33 +00:00
|
|
|
} from '@hyperdx/common-utils/dist/core/metadata';
|
2026-01-21 16:07:30 +00:00
|
|
|
import {
|
|
|
|
|
ChartConfigWithDateRange,
|
|
|
|
|
TSource,
|
|
|
|
|
} from '@hyperdx/common-utils/dist/types';
|
2024-11-12 12:53:15 +00:00
|
|
|
import {
|
|
|
|
|
keepPreviousData,
|
|
|
|
|
useQuery,
|
2026-01-13 12:55:12 +00:00
|
|
|
useQueryClient,
|
2024-11-12 12:53:15 +00:00
|
|
|
UseQueryOptions,
|
|
|
|
|
} from '@tanstack/react-query';
|
|
|
|
|
|
2025-07-29 18:01:26 +00:00
|
|
|
import api from '@/api';
|
2026-01-13 12:55:12 +00:00
|
|
|
import { IS_LOCAL_MODE } from '@/config';
|
|
|
|
|
import { LOCAL_STORE_CONNECTIONS_KEY } from '@/connection';
|
2025-01-21 18:44:14 +00:00
|
|
|
import { getMetadata } from '@/metadata';
|
2026-01-21 16:07:30 +00:00
|
|
|
import { useSource, useSources } from '@/source';
|
2025-04-08 21:36:42 +00:00
|
|
|
import { toArray } from '@/utils';
|
2024-11-12 12:53:15 +00:00
|
|
|
|
2025-09-24 00:42:26 +00:00
|
|
|
// Hook to get metadata with proper settings applied
|
|
|
|
|
export function useMetadataWithSettings() {
|
2026-01-13 12:55:12 +00:00
|
|
|
const [metadata, setMetadata] = useState(getMetadata());
|
2025-09-24 00:42:26 +00:00
|
|
|
const { data: me } = api.useMe();
|
|
|
|
|
const settingsApplied = useRef(false);
|
2026-01-13 12:55:12 +00:00
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
// Create a listener that triggers when connections are updated in local mode
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const isBrowser =
|
|
|
|
|
typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
|
|
|
if (!isBrowser || !IS_LOCAL_MODE) return;
|
|
|
|
|
|
|
|
|
|
const createNewMetadata = (event: StorageEvent) => {
|
|
|
|
|
if (event.key === LOCAL_STORE_CONNECTIONS_KEY && event.newValue) {
|
|
|
|
|
// Create a new metadata instance with a new ClickHouse client,
|
|
|
|
|
// since the existing one will not have connection / auth info.
|
|
|
|
|
setMetadata(getMetadata());
|
|
|
|
|
settingsApplied.current = false;
|
|
|
|
|
// Clear react-query cache so that metadata is refetched with
|
|
|
|
|
// the new connection info, and error states are cleared.
|
|
|
|
|
queryClient.resetQueries();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener('storage', createNewMetadata);
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener('storage', createNewMetadata);
|
|
|
|
|
};
|
|
|
|
|
}, [queryClient]);
|
2025-09-24 00:42:26 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (me?.team?.metadataMaxRowsToRead && !settingsApplied.current) {
|
|
|
|
|
metadata.setClickHouseSettings({
|
|
|
|
|
max_rows_to_read: me.team.metadataMaxRowsToRead,
|
|
|
|
|
});
|
|
|
|
|
settingsApplied.current = true;
|
|
|
|
|
}
|
|
|
|
|
}, [me?.team?.metadataMaxRowsToRead, metadata]);
|
|
|
|
|
|
|
|
|
|
return metadata;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-12 12:53:15 +00:00
|
|
|
export function useColumns(
|
|
|
|
|
{
|
|
|
|
|
databaseName,
|
|
|
|
|
tableName,
|
|
|
|
|
connectionId,
|
|
|
|
|
}: {
|
|
|
|
|
databaseName: string;
|
|
|
|
|
tableName: string;
|
|
|
|
|
connectionId: string;
|
|
|
|
|
},
|
|
|
|
|
options?: Partial<UseQueryOptions<ColumnMeta[]>>,
|
|
|
|
|
) {
|
2025-09-24 00:42:26 +00:00
|
|
|
const metadata = useMetadataWithSettings();
|
2024-11-12 12:53:15 +00:00
|
|
|
return useQuery<ColumnMeta[]>({
|
|
|
|
|
queryKey: ['useMetadata.useColumns', { databaseName, tableName }],
|
|
|
|
|
queryFn: async () => {
|
|
|
|
|
return metadata.getColumns({
|
|
|
|
|
databaseName,
|
|
|
|
|
tableName,
|
|
|
|
|
connectionId,
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-07-16 16:30:25 +00:00
|
|
|
enabled: !!databaseName && !!tableName && !!connectionId,
|
2024-11-12 12:53:15 +00:00
|
|
|
...options,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 11:32:24 +00:00
|
|
|
export function useJsonColumns(
|
2025-10-02 21:33:53 +00:00
|
|
|
tableConnection: TableConnection | undefined,
|
2025-08-08 11:32:24 +00:00
|
|
|
options?: Partial<UseQueryOptions<string[]>>,
|
|
|
|
|
) {
|
2025-09-24 00:42:26 +00:00
|
|
|
const metadata = useMetadataWithSettings();
|
2025-08-08 11:32:24 +00:00
|
|
|
return useQuery<string[]>({
|
2025-10-02 21:33:53 +00:00
|
|
|
queryKey: ['useMetadata.useJsonColumns', tableConnection],
|
2025-08-08 11:32:24 +00:00
|
|
|
queryFn: async () => {
|
2025-10-02 21:33:53 +00:00
|
|
|
if (!tableConnection) return [];
|
|
|
|
|
const columns = await metadata.getColumns(tableConnection);
|
2025-08-08 11:32:24 +00:00
|
|
|
return (
|
|
|
|
|
filterColumnMetaByType(columns, [JSDataType.JSON])?.map(
|
|
|
|
|
column => column.name,
|
|
|
|
|
) ?? []
|
|
|
|
|
);
|
|
|
|
|
},
|
2025-10-02 21:33:53 +00:00
|
|
|
enabled:
|
|
|
|
|
tableConnection &&
|
|
|
|
|
!!tableConnection.databaseName &&
|
|
|
|
|
!!tableConnection.tableName &&
|
|
|
|
|
!!tableConnection.connectionId,
|
2025-08-08 11:32:24 +00:00
|
|
|
...options,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-02 21:33:53 +00:00
|
|
|
export function useMultipleAllFields(
|
|
|
|
|
tableConnections: TableConnection[],
|
2024-11-12 12:53:15 +00:00
|
|
|
options?: Partial<UseQueryOptions<Field[]>>,
|
|
|
|
|
) {
|
2025-09-24 00:42:26 +00:00
|
|
|
const metadata = useMetadataWithSettings();
|
2025-07-29 18:01:26 +00:00
|
|
|
const { data: me, isFetched } = api.useMe();
|
2024-11-12 12:53:15 +00:00
|
|
|
return useQuery<Field[]>({
|
2025-03-27 19:17:29 +00:00
|
|
|
queryKey: [
|
2025-10-02 21:33:53 +00:00
|
|
|
'useMetadata.useMultipleAllFields',
|
2025-03-27 19:17:29 +00:00
|
|
|
...tableConnections.map(tc => ({ ...tc })),
|
|
|
|
|
],
|
2024-11-12 12:53:15 +00:00
|
|
|
queryFn: async () => {
|
2025-08-07 23:54:35 +00:00
|
|
|
const team = me?.team;
|
|
|
|
|
if (team?.fieldMetadataDisabled) {
|
2025-07-29 13:59:47 +00:00
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-27 19:17:29 +00:00
|
|
|
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);
|
2024-11-12 12:53:15 +00:00
|
|
|
},
|
2025-07-16 16:30:25 +00:00
|
|
|
enabled:
|
|
|
|
|
tableConnections.length > 0 &&
|
|
|
|
|
tableConnections.every(
|
|
|
|
|
tc => !!tc.databaseName && !!tc.tableName && !!tc.connectionId,
|
2025-07-29 18:01:26 +00:00
|
|
|
) &&
|
|
|
|
|
isFetched,
|
2024-11-12 12:53:15 +00:00
|
|
|
...options,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-02 21:33:53 +00:00
|
|
|
export function useAllFields(
|
|
|
|
|
tableConnection: TableConnection | undefined,
|
|
|
|
|
options?: Partial<UseQueryOptions<Field[]>>,
|
|
|
|
|
) {
|
|
|
|
|
return useMultipleAllFields(
|
|
|
|
|
tableConnection ? [tableConnection] : [],
|
|
|
|
|
options,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-10 07:30:40 +00:00
|
|
|
export function useTableMetadata(
|
2024-11-12 12:53:15 +00:00
|
|
|
{
|
|
|
|
|
databaseName,
|
|
|
|
|
tableName,
|
|
|
|
|
connectionId,
|
|
|
|
|
}: {
|
|
|
|
|
databaseName: string;
|
|
|
|
|
tableName: string;
|
|
|
|
|
connectionId: string;
|
|
|
|
|
},
|
|
|
|
|
options?: Omit<UseQueryOptions<any, Error>, 'queryKey'>,
|
|
|
|
|
) {
|
2025-09-24 00:42:26 +00:00
|
|
|
const metadata = useMetadataWithSettings();
|
2024-12-10 07:30:40 +00:00
|
|
|
return useQuery<TableMetadata>({
|
|
|
|
|
queryKey: ['useMetadata.useTableMetadata', { databaseName, tableName }],
|
2024-11-12 12:53:15 +00:00
|
|
|
queryFn: async () => {
|
2024-12-10 07:30:40 +00:00
|
|
|
return await metadata.getTableMetadata({
|
2024-11-12 12:53:15 +00:00
|
|
|
databaseName,
|
|
|
|
|
tableName,
|
|
|
|
|
connectionId,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
staleTime: 1000 * 60 * 5, // Cache every 5 min
|
2025-07-16 16:30:25 +00:00
|
|
|
enabled: !!databaseName && !!tableName && !!connectionId,
|
2024-11-12 12:53:15 +00:00
|
|
|
...options,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-02 21:33:53 +00:00
|
|
|
export function useMultipleGetKeyValues(
|
2025-03-28 20:44:38 +00:00
|
|
|
{
|
2025-04-08 21:36:42 +00:00
|
|
|
chartConfigs,
|
2025-03-28 20:44:38 +00:00
|
|
|
keys,
|
|
|
|
|
limit,
|
|
|
|
|
disableRowLimit,
|
|
|
|
|
}: {
|
2025-04-08 21:36:42 +00:00
|
|
|
chartConfigs: ChartConfigWithDateRange | ChartConfigWithDateRange[];
|
2025-03-28 20:44:38 +00:00
|
|
|
keys: string[];
|
|
|
|
|
limit?: number;
|
|
|
|
|
disableRowLimit?: boolean;
|
|
|
|
|
},
|
|
|
|
|
options?: Omit<UseQueryOptions<any, Error>, 'queryKey'>,
|
|
|
|
|
) {
|
2025-09-24 00:42:26 +00:00
|
|
|
const metadata = useMetadataWithSettings();
|
2025-04-08 21:36:42 +00:00
|
|
|
const chartConfigsArr = toArray(chartConfigs);
|
2026-01-14 18:05:11 +00:00
|
|
|
|
|
|
|
|
const { enabled = true } = options || {};
|
|
|
|
|
const { data: sources, isLoading: isLoadingSources } = useSources();
|
|
|
|
|
|
|
|
|
|
const query = useQuery<{ key: string; value: string[] }[]>({
|
2025-04-08 21:36:42 +00:00
|
|
|
queryKey: [
|
|
|
|
|
'useMetadata.useGetKeyValues',
|
|
|
|
|
...chartConfigsArr.map(cc => ({ ...cc })),
|
|
|
|
|
...keys,
|
2025-05-19 13:43:19 +00:00
|
|
|
disableRowLimit,
|
2025-04-08 21:36:42 +00:00
|
|
|
],
|
2026-01-14 18:05:11 +00:00
|
|
|
queryFn: async ({ signal }) => {
|
2025-08-07 23:54:35 +00:00
|
|
|
return (
|
2025-04-08 21:36:42 +00:00
|
|
|
await Promise.all(
|
2026-01-14 18:05:11 +00:00
|
|
|
chartConfigsArr.map(chartConfig => {
|
|
|
|
|
const source = chartConfig.source
|
|
|
|
|
? sources?.find(s => s.id === chartConfig.source)
|
|
|
|
|
: undefined;
|
|
|
|
|
return metadata.getKeyValuesWithMVs({
|
2025-04-08 21:36:42 +00:00
|
|
|
chartConfig,
|
|
|
|
|
keys: keys.slice(0, 20), // Limit to 20 keys for now, otherwise request fails (max header size)
|
|
|
|
|
limit,
|
|
|
|
|
disableRowLimit,
|
2026-01-14 18:05:11 +00:00
|
|
|
source,
|
|
|
|
|
signal,
|
|
|
|
|
});
|
|
|
|
|
}),
|
2025-04-08 21:36:42 +00:00
|
|
|
)
|
2025-08-07 23:54:35 +00:00
|
|
|
).flatMap(v => v);
|
|
|
|
|
},
|
2024-11-12 12:53:15 +00:00
|
|
|
staleTime: 1000 * 60 * 5, // Cache every 5 min
|
|
|
|
|
placeholderData: keepPreviousData,
|
2025-03-28 20:44:38 +00:00
|
|
|
...options,
|
2026-01-14 18:05:11 +00:00
|
|
|
enabled: !!enabled && !!keys.length && !isLoadingSources,
|
2024-11-12 12:53:15 +00:00
|
|
|
});
|
2026-01-14 18:05:11 +00:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...query,
|
|
|
|
|
isLoading: query.isLoading || isLoadingSources,
|
|
|
|
|
};
|
2024-11-12 12:53:15 +00:00
|
|
|
}
|
2025-03-27 19:17:29 +00:00
|
|
|
|
2025-10-09 19:26:39 +00:00
|
|
|
export function useGetValuesDistribution(
|
|
|
|
|
{
|
|
|
|
|
chartConfig,
|
|
|
|
|
key,
|
|
|
|
|
limit,
|
|
|
|
|
}: {
|
|
|
|
|
chartConfig: ChartConfigWithDateRange;
|
|
|
|
|
key: string;
|
|
|
|
|
limit: number;
|
|
|
|
|
},
|
|
|
|
|
options?: Omit<UseQueryOptions<Map<string, number>, Error>, 'queryKey'>,
|
|
|
|
|
) {
|
|
|
|
|
const metadata = useMetadataWithSettings();
|
2026-01-21 16:07:30 +00:00
|
|
|
const { data: source, isLoading: isLoadingSource } = useSource({
|
|
|
|
|
id: chartConfig.source,
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-09 19:26:39 +00:00
|
|
|
return useQuery<Map<string, number>>({
|
|
|
|
|
queryKey: ['useMetadata.useGetValuesDistribution', chartConfig, key],
|
|
|
|
|
queryFn: async () => {
|
|
|
|
|
return await metadata.getValuesDistribution({
|
|
|
|
|
chartConfig,
|
|
|
|
|
key,
|
|
|
|
|
limit,
|
2026-01-21 16:07:30 +00:00
|
|
|
source,
|
2025-10-09 19:26:39 +00:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
staleTime: Infinity,
|
2026-01-21 16:07:30 +00:00
|
|
|
enabled: !!key && !isLoadingSource,
|
2025-10-09 19:26:39 +00:00
|
|
|
placeholderData: keepPreviousData,
|
|
|
|
|
retry: false,
|
|
|
|
|
...options,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-02 21:33:53 +00:00
|
|
|
export function useGetKeyValues(
|
|
|
|
|
{
|
|
|
|
|
chartConfig,
|
|
|
|
|
keys,
|
|
|
|
|
limit,
|
|
|
|
|
disableRowLimit,
|
|
|
|
|
}: {
|
|
|
|
|
chartConfig?: ChartConfigWithDateRange;
|
|
|
|
|
keys: string[];
|
|
|
|
|
limit?: number;
|
|
|
|
|
disableRowLimit?: boolean;
|
|
|
|
|
},
|
|
|
|
|
options?: Omit<UseQueryOptions<any, Error>, 'queryKey'>,
|
|
|
|
|
) {
|
|
|
|
|
return useMultipleGetKeyValues(
|
|
|
|
|
{
|
|
|
|
|
chartConfigs: chartConfig ? [chartConfig] : [],
|
|
|
|
|
keys,
|
|
|
|
|
limit,
|
|
|
|
|
disableRowLimit,
|
|
|
|
|
},
|
|
|
|
|
options,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-08 21:36:42 +00:00
|
|
|
export function deduplicateArray<T extends object>(array: T[]): T[] {
|
|
|
|
|
return deduplicate2dArray([array]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-27 19:17:29 +00:00
|
|
|
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;
|
|
|
|
|
}
|