mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
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:
parent
0d9f3fe04e
commit
0183483a9a
5 changed files with 219 additions and 16 deletions
5
.changeset/strange-eagles-roll.md
Normal file
5
.changeset/strange-eagles-roll.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
feat: Add source schema previews
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) ? (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
170
packages/app/src/components/SourceSchemaPreview.tsx
Normal file
170
packages/app/src/components/SourceSchemaPreview.tsx
Normal 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;
|
||||
Loading…
Reference in a new issue