feat: Add source schema previews (#1182)

Closes HDX-2404

This PR adds a new info icon next to various Source pickers, which when clicked opens a modal that shows the schema(s) for the table(s) associated with the source.

<details>
<summary>On the search page</summary>
<img width="483" height="263" alt="Screenshot 2025-09-18 at 1 57 14 PM" src="https://github.com/user-attachments/assets/84437e6d-f9da-4885-af87-b72767681b61" />
<img width="1367" height="923" alt="Screenshot 2025-09-18 at 4 15 12 PM" src="https://github.com/user-attachments/assets/1fe259f7-2cbf-480b-b3c3-5e94a298dd07" />
</details>

<details>
<summary>In the source form</summary>
<img width="1122" height="657" alt="Screenshot 2025-09-18 at 1 57 57 PM" src="https://github.com/user-attachments/assets/0ffa3bfb-46df-45e6-8a64-188f52d7d1cb" />
<img width="1244" height="520" alt="Screenshot 2025-09-18 at 1 58 11 PM" src="https://github.com/user-attachments/assets/0c4fb035-afb0-4eda-8bdc-3d8b3ccd34c9" />
</details>

<details>
<summary>In the chart explorer</summary>
<img width="559" height="221" alt="Screenshot 2025-09-18 at 1 57 33 PM" src="https://github.com/user-attachments/assets/8ea84e73-eb5d-445a-9faa-0180b5b9b8f9" />
</details>

<details>
<summary>Multiple schemas are shown when a metric source is chosen</summary>
<img width="890" height="1044" alt="Screenshot 2025-09-18 at 4 14 37 PM" src="https://github.com/user-attachments/assets/f2463435-e9f5-4253-a3cb-2c76a74ea18e" />
</details>
This commit is contained in:
Drew Davis 2025-09-19 11:02:15 -04:00 committed by GitHub
parent 0d9f3fe04e
commit 0183483a9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 219 additions and 16 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
feat: Add source schema previews

View file

@ -101,6 +101,7 @@ import { QUERY_LOCAL_STORAGE, useLocalStorage, usePrevious } from '@/utils';
import { SQLPreview } from './components/ChartSQLPreview';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import PatternTable from './components/PatternTable';
import SourceSchemaPreview from './components/SourceSchemaPreview';
import { useTableMetadata } from './hooks/useMetadata';
import { useSqlSuggestions } from './hooks/useSqlSuggestions';
import api from './api';
@ -1223,6 +1224,12 @@ function DBSearchPage() {
onCreate={openNewSourceModal}
allowedSourceKinds={[SourceKind.Log, SourceKind.Trace]}
/>
<span className="ms-1">
<SourceSchemaPreview
source={inputSourceObj}
iconStyles={{ size: 'xs', color: 'dark.2' }}
/>
</span>
<Menu withArrow position="bottom-start">
<Menu.Target>
<ActionIcon

View file

@ -82,6 +82,7 @@ import {
} from './InputControlled';
import { MetricNameSelect } from './MetricNameSelect';
import { NumberFormatInput } from './NumberFormat';
import SourceSchemaPreview from './SourceSchemaPreview';
import { SourceSelectControlled } from './SourceSelect';
const isQueryReady = (queriedConfig: ChartConfigWithDateRange | undefined) =>
@ -671,6 +672,10 @@ export default function EditTimeChartForm({
Data Source
</Text>
<SourceSelectControlled size="xs" control={control} name="source" />
<SourceSchemaPreview
source={tableSource}
iconStyles={{ color: 'dark.2' }}
/>
</Flex>
{displayType !== DisplayType.Search && Array.isArray(select) ? (

View file

@ -1,8 +1,10 @@
import { useController, UseControllerProps } from 'react-hook-form';
import { Select } from '@mantine/core';
import { Flex, Select } from '@mantine/core';
import { useTablesDirect } from '@/clickhouse';
import SourceSchemaPreview from './SourceSchemaPreview';
export default function DBTableSelect({
database,
setTable,
@ -35,21 +37,35 @@ export default function DBTableSelect({
}));
return (
<Select
searchable
placeholder="Table"
leftSection={<i className="bi bi-table"></i>}
maxDropdownHeight={280}
data={data}
disabled={isTablesLoading}
value={table}
comboboxProps={{ withinPortal: false }}
onChange={v => setTable(v ?? undefined)}
onBlur={onBlur}
name={name}
ref={inputRef}
size={size}
/>
<Flex align="center" gap={8}>
<Select
searchable
placeholder="Table"
leftSection={<i className="bi bi-table"></i>}
maxDropdownHeight={280}
data={data}
disabled={isTablesLoading}
value={table}
comboboxProps={{ withinPortal: false }}
onChange={v => setTable(v ?? undefined)}
onBlur={onBlur}
name={name}
ref={inputRef}
size={size}
className="flex-grow-1"
/>
<SourceSchemaPreview
source={
connectionId && database && table
? {
connection: connectionId,
from: { databaseName: database, tableName: table },
}
: undefined
}
iconStyles={{ color: 'gray.4' }}
/>
</Flex>
);
}

View file

@ -0,0 +1,170 @@
import { useState } from 'react';
import { MetricsDataType, TSource } from '@hyperdx/common-utils/dist/types';
import { Modal, Paper, Tabs, Text, TextProps, Tooltip } from '@mantine/core';
import { useTableMetadata } from '@/hooks/useMetadata';
import { SQLPreview } from './ChartSQLPreview';
interface SourceSchemaInfoIconProps {
onClick: () => void;
isEnabled: boolean;
tableCount: number;
iconStyles?: Pick<TextProps, 'size' | 'color'>;
}
const SourceSchemaInfoIcon = ({
onClick,
isEnabled,
tableCount,
iconStyles,
}: SourceSchemaInfoIconProps) => {
const tooltipText = isEnabled
? tableCount > 1
? `Show Table Schemas`
: 'Show Table Schema'
: 'Select a table to view its schema';
return (
<Tooltip
label={tooltipText}
color="dark"
c="white"
position="right"
onClick={() => isEnabled && onClick()}
>
<Text {...iconStyles}>
<i
className={`bi bi-info-circle ${isEnabled ? 'cursor-pointer' : ''}`}
/>
</Text>
</Tooltip>
);
};
interface TableSchemaPreviewProps {
databaseName: string;
tableName: string;
connectionId: string;
}
const TableSchemaPreview = ({
databaseName,
tableName,
connectionId,
}: TableSchemaPreviewProps) => {
const { data, isLoading } = useTableMetadata({
databaseName,
tableName,
connectionId,
});
return (
<Paper flex="auto" shadow="none" radius="sm" style={{ overflow: 'hidden' }}>
{isLoading ? (
<div className="spin-animate d-inline-block">
<i className="bi bi-arrow-repeat" />
</div>
) : (
<SQLPreview
data={data?.create_table_query ?? 'Schema is not available'}
enableCopy={!!data?.create_table_query}
/>
)}
</Paper>
);
};
export interface SourceSchemaPreviewProps {
source?: Pick<TSource, 'connection' | 'from' | 'metricTables'> &
Partial<Pick<TSource, 'kind' | 'name'>>;
iconStyles?: Pick<TextProps, 'size' | 'color'>;
}
const METRIC_TYPE_NAMES: Record<MetricsDataType, string> = {
[MetricsDataType.Sum]: 'Sum',
[MetricsDataType.Gauge]: 'Gauge',
[MetricsDataType.Histogram]: 'Histogram',
[MetricsDataType.Summary]: 'Summary',
[MetricsDataType.ExponentialHistogram]: 'Exponential Histogram',
};
const SourceSchemaPreview = ({
source,
iconStyles,
}: SourceSchemaPreviewProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const isMetricSource = source?.kind === 'metric';
const tables: (TableSchemaPreviewProps & { title: string })[] = [];
if (source && isMetricSource) {
tables.push(
...Object.values(MetricsDataType)
.map(metricType => ({
metricType,
tableName: source.metricTables?.[metricType],
}))
.filter(({ tableName }) => !!tableName)
.map(({ metricType, tableName }) => ({
databaseName: source.from.databaseName,
tableName: tableName!,
connectionId: source.connection,
title: METRIC_TYPE_NAMES[metricType],
})),
);
} else if (source && source.from.tableName) {
tables.push({
databaseName: source.from.databaseName,
tableName: source.from.tableName,
connectionId: source.connection,
title: source.name ?? source.from.tableName,
});
}
const isEnabled = !!source && tables.length > 0;
return (
<>
<SourceSchemaInfoIcon
isEnabled={isEnabled}
onClick={() => setIsModalOpen(true)}
iconStyles={iconStyles}
tableCount={tables.length}
/>
{isEnabled && (
<Modal
opened={isModalOpen}
onClose={() => setIsModalOpen(false)}
size="auto"
title={tables.length > 1 ? `Table Schemas` : `Table Schema`}
>
<Tabs
defaultValue={`${tables[0]?.databaseName}.${tables[0]?.tableName}.${tables[0]?.title}`}
>
<Tabs.List>
{tables.map(table => (
<Tabs.Tab
key={`${table.databaseName}.${table.tableName}.${table.title}`}
value={`${table.databaseName}.${table.tableName}.${table.title}`}
>
{table.title}
</Tabs.Tab>
))}
</Tabs.List>
{tables.map(table => (
<Tabs.Panel
key={`${table.databaseName}.${table.tableName}.${table.title}`}
value={`${table.databaseName}.${table.tableName}.${table.title}`}
pt="sm"
>
<TableSchemaPreview {...table} />
</Tabs.Panel>
))}
</Tabs>
</Modal>
)}
</>
);
};
export default SourceSchemaPreview;