mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
fix: bypass aliasWith so that useRowWhere works correctly (#1623)
Revisit the bug fix for https://github.com/hyperdxio/hyperdx/pull/1614. The alias map should be used in useRowWhere hook Ref: HDX-3196 Example: For select ``` Timestamp,ServiceName,SeverityText,Body AS b, concat(b, 'blabla') ``` The generated query from useRowWhere is ``` WITH (Body) AS b SELECT *, Timestamp AS "__hdx_timestamp", Body AS "__hdx_body", TraceId AS "__hdx_trace_id", SpanId AS "__hdx_span_id", SeverityText AS "__hdx_severity_text", ServiceName AS "__hdx_service_name", ResourceAttributes AS "__hdx_resource_attributes", LogAttributes AS "__hdx_event_attributes" FROM DEFAULT.otel_logs WHERE ( Timestamp = parseDateTime64BestEffort('2026-01-20T06:11:00.170000000Z', 9) AND ServiceName = 'hdx-oss-dev-api' AND SeverityText = 'info' AND Body = 'Received alert metric [saved_search source]' AND concat(b, 'blabla') = 'Received alert metric [saved_search source]blabla' AND TimestampTime = parseDateTime64BestEffort('2026-01-20T06:11:00Z', 9) ) LIMIT 1 ```
This commit is contained in:
parent
ddc54e43f0
commit
db845604a2
22 changed files with 273 additions and 106 deletions
5
.changeset/gorgeous-beers-hope.md
Normal file
5
.changeset/gorgeous-beers-hope.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
fix: bypass aliasWith so that useRowWhere works correctly
|
||||
|
|
@ -92,6 +92,7 @@ import WhereLanguageControlled from '@/components/WhereLanguageControlled';
|
|||
import { IS_LOCAL_MODE } from '@/config';
|
||||
import { useAliasMapFromChartConfig } from '@/hooks/useChartConfig';
|
||||
import { useExplainQuery } from '@/hooks/useExplainQuery';
|
||||
import { aliasMapToWithClauses } from '@/hooks/useRowWhere';
|
||||
import { withAppNav } from '@/layout';
|
||||
import {
|
||||
useCreateSavedSearch,
|
||||
|
|
@ -1348,18 +1349,7 @@ function DBSearchPage() {
|
|||
|
||||
const { data: aliasMap } = useAliasMapFromChartConfig(dbSqlRowTableConfig);
|
||||
|
||||
const aliasWith = useMemo(
|
||||
() =>
|
||||
Object.entries(aliasMap ?? {}).map(([key, value]) => ({
|
||||
name: key,
|
||||
sql: {
|
||||
sql: value,
|
||||
params: {},
|
||||
},
|
||||
isSubquery: false,
|
||||
})),
|
||||
[aliasMap],
|
||||
);
|
||||
const aliasWith = useMemo(() => aliasMapToWithClauses(aliasMap), [aliasMap]);
|
||||
|
||||
const histogramTimeChartConfig = useMemo(() => {
|
||||
if (chartConfig == null) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import DBRowSidePanel from '@/components/DBRowSidePanel';
|
|||
import { DBTimeChart } from '@/components/DBTimeChart';
|
||||
import { DrawerBody, DrawerHeader } from '@/components/DrawerUtils';
|
||||
import { KubeTimeline, useV2LogBatch } from '@/components/KubeComponents';
|
||||
import { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
|
||||
import { parseTimeQuery, useTimeQuery } from '@/timeQuery';
|
||||
import { useZIndex, ZIndexContext } from '@/zIndex';
|
||||
|
||||
|
|
@ -136,7 +137,7 @@ function PodLogs({
|
|||
logSource: TSource;
|
||||
where: string;
|
||||
rowId: string | null;
|
||||
onRowClick: (rowId: string) => void;
|
||||
onRowClick: (rowWhere: RowWhereResult) => void;
|
||||
}) {
|
||||
const [resultType, setResultType] = React.useState<'all' | 'error'>('all');
|
||||
|
||||
|
|
@ -231,8 +232,10 @@ export default function PodDetailsSidePanel({
|
|||
);
|
||||
|
||||
const [rowId, setRowId] = React.useState<string | null>(null);
|
||||
const handleRowClick = React.useCallback((rowWhere: string) => {
|
||||
setRowId(rowWhere);
|
||||
const [aliasWith, setAliasWith] = React.useState<WithClause[]>([]);
|
||||
const handleRowClick = React.useCallback((rowWhere: RowWhereResult) => {
|
||||
setRowId(rowWhere.where);
|
||||
setAliasWith(rowWhere.aliasWith);
|
||||
}, []);
|
||||
const handleCloseRowSidePanel = React.useCallback(() => {
|
||||
setRowId(null);
|
||||
|
|
@ -466,6 +469,7 @@ export default function PodDetailsSidePanel({
|
|||
<DBRowSidePanel
|
||||
source={logSource}
|
||||
rowId={rowId}
|
||||
aliasWith={aliasWith}
|
||||
onClose={handleCloseRowSidePanel}
|
||||
isNestedPanel={true}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from '@tabler/icons-react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
|
||||
import useRowWhere from '@/hooks/useRowWhere';
|
||||
import useRowWhere, { RowWhereResult } from '@/hooks/useRowWhere';
|
||||
|
||||
import { useQueriedChartConfig } from './hooks/useChartConfig';
|
||||
import { useFormatTime } from './useFormatTime';
|
||||
|
|
@ -108,7 +108,7 @@ export const SessionEventList = ({
|
|||
focus: { ts: number; setBy: string } | undefined;
|
||||
minTs: number;
|
||||
showRelativeTime: boolean;
|
||||
onClick: (rowId: string) => void;
|
||||
onClick: (rowWhere: RowWhereResult) => void;
|
||||
onTimeClick: (ts: number) => void;
|
||||
eventsFollowPlayerPosition: boolean;
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
} from '@tabler/icons-react';
|
||||
|
||||
import DBRowSidePanel from '@/components/DBRowSidePanel';
|
||||
import { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
|
||||
|
||||
import { SQLInlineEditorControlled } from './components/SQLInlineEditor';
|
||||
import DOMPlayer from './DOMPlayer';
|
||||
|
|
@ -79,6 +80,7 @@ export default function SessionSubpanel({
|
|||
whereLanguage?: SearchConditionLanguage;
|
||||
}) {
|
||||
const [rowId, setRowId] = useState<string | undefined>(undefined);
|
||||
const [aliasWith, setAliasWith] = useState<WithClause[]>([]);
|
||||
|
||||
// Without portaling the nested drawer close overlay will not render properly
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
@ -103,6 +105,7 @@ export default function SessionSubpanel({
|
|||
<DBRowSidePanel
|
||||
source={traceSource}
|
||||
rowId={rowId}
|
||||
aliasWith={aliasWith}
|
||||
onClose={() => {
|
||||
setDrawerOpen(false);
|
||||
setRowId(undefined);
|
||||
|
|
@ -551,11 +554,12 @@ export default function SessionSubpanel({
|
|||
aliasMap={aliasMap}
|
||||
queriedConfig={sessionEventListConfig}
|
||||
onClick={useCallback(
|
||||
(id: string) => {
|
||||
(rowWhere: RowWhereResult) => {
|
||||
setDrawerOpen(true);
|
||||
setRowId(id);
|
||||
setRowId(rowWhere.where);
|
||||
setAliasWith(rowWhere.aliasWith);
|
||||
},
|
||||
[setDrawerOpen, setRowId],
|
||||
[setDrawerOpen, setRowId, setAliasWith],
|
||||
)}
|
||||
focus={focus}
|
||||
onTimeClick={useCallback(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Paper } from '@mantine/core';
|
|||
|
||||
import { DBTimeChart } from '@/components/DBTimeChart';
|
||||
import { useAliasMapFromChartConfig } from '@/hooks/useChartConfig';
|
||||
import { aliasMapToWithClauses } from '@/hooks/useRowWhere';
|
||||
import { intervalToDateRange, intervalToGranularity } from '@/utils/alerts';
|
||||
|
||||
import { getAlertReferenceLines } from './Alerts';
|
||||
|
|
@ -46,14 +47,7 @@ export const AlertPreviewChart = ({
|
|||
from: source.from,
|
||||
whereLanguage: whereLanguage || undefined,
|
||||
});
|
||||
const aliasWith = Object.entries(aliasMap ?? {}).map(([key, value]) => ({
|
||||
name: key,
|
||||
sql: {
|
||||
sql: value,
|
||||
params: {},
|
||||
},
|
||||
isSubquery: false,
|
||||
}));
|
||||
const aliasWith = aliasMapToWithClauses(aliasMap);
|
||||
|
||||
return (
|
||||
<Paper w="100%" h={200}>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useDebouncedValue } from '@mantine/hooks';
|
|||
|
||||
import { SQLInlineEditorControlled } from '@/components/SQLInlineEditor';
|
||||
import WhereLanguageControlled from '@/components/WhereLanguageControlled';
|
||||
import { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
|
||||
import SearchInputV2 from '@/SearchInputV2';
|
||||
import { useSource } from '@/source';
|
||||
import { formatAttributeClause } from '@/utils';
|
||||
|
|
@ -106,6 +107,8 @@ export default function ContextSubpanel({
|
|||
id: contextRowSource || '',
|
||||
});
|
||||
|
||||
const [contextAliasWith, setContextAliasWith] = useState<WithClause[]>([]);
|
||||
|
||||
const handleContextSidePanelClose = useCallback(() => {
|
||||
setContextRowId(null);
|
||||
setContextRowSource(null);
|
||||
|
|
@ -114,8 +117,9 @@ export default function ContextSubpanel({
|
|||
const { setChildModalOpen } = useContext(RowSidePanelContext);
|
||||
|
||||
const handleRowExpandClick = useCallback(
|
||||
(rowWhere: string) => {
|
||||
setContextRowId(rowWhere);
|
||||
(rowWhere: RowWhereResult) => {
|
||||
setContextRowId(rowWhere.where);
|
||||
setContextAliasWith(rowWhere.aliasWith);
|
||||
setContextRowSource(source.id);
|
||||
},
|
||||
[source.id, setContextRowId, setContextRowSource],
|
||||
|
|
@ -334,6 +338,7 @@ export default function ContextSubpanel({
|
|||
<DBRowSidePanel
|
||||
source={contextRowSidePanelSource}
|
||||
rowId={contextRowId}
|
||||
aliasWith={contextAliasWith}
|
||||
onClose={handleContextSidePanelClose}
|
||||
isNestedPanel={true}
|
||||
breadcrumbPath={[
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
|
|||
import { Box } from '@mantine/core';
|
||||
|
||||
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
|
||||
import { WithClause } from '@/hooks/useRowWhere';
|
||||
import { getDisplayedTimestampValueExpression, getEventBody } from '@/source';
|
||||
import { getSelectExpressionsForHighlightedAttributes } from '@/utils/highlightedAttributes';
|
||||
|
||||
|
|
@ -26,9 +27,11 @@ export enum ROW_DATA_ALIASES {
|
|||
export function useRowData({
|
||||
source,
|
||||
rowId,
|
||||
aliasWith,
|
||||
}: {
|
||||
source: TSource;
|
||||
rowId: string | undefined | null;
|
||||
aliasWith?: WithClause[];
|
||||
}) {
|
||||
const eventBodyExpr = getEventBody(source);
|
||||
|
||||
|
|
@ -129,9 +132,10 @@ export function useRowData({
|
|||
where: rowId ?? '0=1',
|
||||
from: source.from,
|
||||
limit: { limit: 1 },
|
||||
...(aliasWith && aliasWith.length > 0 ? { with: aliasWith } : {}),
|
||||
},
|
||||
{
|
||||
queryKey: ['row_side_panel', rowId, source],
|
||||
queryKey: ['row_side_panel', rowId, aliasWith, source],
|
||||
enabled: rowId != null,
|
||||
},
|
||||
);
|
||||
|
|
@ -182,13 +186,15 @@ export function getJSONColumnNames(meta: ResponseJSON['meta'] | undefined) {
|
|||
export function RowDataPanel({
|
||||
source,
|
||||
rowId,
|
||||
aliasWith,
|
||||
'data-testid': dataTestId,
|
||||
}: {
|
||||
source: TSource;
|
||||
rowId: string | undefined | null;
|
||||
aliasWith?: WithClause[];
|
||||
'data-testid'?: string;
|
||||
}) {
|
||||
const { data, isLoading, isError } = useRowData({ source, rowId });
|
||||
const { data, isLoading, isError } = useRowData({ source, rowId, aliasWith });
|
||||
|
||||
const firstRow = useMemo(() => {
|
||||
const firstRow = { ...(data?.data?.[0] ?? {}) };
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import pickBy from 'lodash/pickBy';
|
|||
import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
|
||||
import { Accordion, Box, Flex, Text } from '@mantine/core';
|
||||
|
||||
import { WithClause } from '@/hooks/useRowWhere';
|
||||
import { getEventBody } from '@/source';
|
||||
import { getHighlightedAttributesFromData } from '@/utils/highlightedAttributes';
|
||||
|
||||
|
|
@ -20,15 +21,17 @@ const EMPTY_OBJ = {};
|
|||
export function RowOverviewPanel({
|
||||
source,
|
||||
rowId,
|
||||
aliasWith,
|
||||
hideHeader = false,
|
||||
'data-testid': dataTestId,
|
||||
}: {
|
||||
source: TSource;
|
||||
rowId: string | undefined | null;
|
||||
aliasWith?: WithClause[];
|
||||
hideHeader?: boolean;
|
||||
'data-testid'?: string;
|
||||
}) {
|
||||
const { data } = useRowData({ source, rowId });
|
||||
const { data } = useRowData({ source, rowId, aliasWith });
|
||||
const { onPropertyAddClick, generateSearchUrl } =
|
||||
useContext(RowSidePanelContext);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import DBRowSidePanelHeader, {
|
|||
BreadcrumbPath,
|
||||
} from '@/components/DBRowSidePanelHeader';
|
||||
import useResizable from '@/hooks/useResizable';
|
||||
import { WithClause } from '@/hooks/useRowWhere';
|
||||
import useWaterfallSearchState from '@/hooks/useWaterfallSearchState';
|
||||
import { LogSidePanelKbdShortcuts } from '@/LogSidePanelElements';
|
||||
import { getEventBody } from '@/source';
|
||||
|
|
@ -84,6 +85,7 @@ enum Tab {
|
|||
type DBRowSidePanelProps = {
|
||||
source: TSource;
|
||||
rowId: string | undefined;
|
||||
aliasWith?: WithClause[];
|
||||
onClose: () => void;
|
||||
isNestedPanel?: boolean;
|
||||
breadcrumbPath?: BreadcrumbPath;
|
||||
|
|
@ -92,6 +94,7 @@ type DBRowSidePanelProps = {
|
|||
|
||||
const DBRowSidePanel = ({
|
||||
rowId: rowId,
|
||||
aliasWith,
|
||||
source,
|
||||
isNestedPanel = false,
|
||||
setSubDrawerOpen,
|
||||
|
|
@ -108,6 +111,7 @@ const DBRowSidePanel = ({
|
|||
} = useRowData({
|
||||
source,
|
||||
rowId,
|
||||
aliasWith,
|
||||
});
|
||||
|
||||
const { dbSqlRowTableConfig } = useContext(RowSidePanelContext);
|
||||
|
|
@ -377,6 +381,7 @@ const DBRowSidePanel = ({
|
|||
data-testid="side-panel-tab-overview"
|
||||
source={source}
|
||||
rowId={rowId}
|
||||
aliasWith={aliasWith}
|
||||
hideHeader={true}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
|
@ -440,6 +445,7 @@ const DBRowSidePanel = ({
|
|||
data-testid="side-panel-tab-parsed"
|
||||
source={source}
|
||||
rowId={rowId}
|
||||
aliasWith={aliasWith}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
|
|
@ -518,6 +524,7 @@ const DBRowSidePanel = ({
|
|||
export default function DBRowSidePanelErrorBoundary({
|
||||
onClose,
|
||||
rowId,
|
||||
aliasWith,
|
||||
source,
|
||||
isNestedPanel,
|
||||
breadcrumbPath = [],
|
||||
|
|
@ -594,6 +601,7 @@ export default function DBRowSidePanelErrorBoundary({
|
|||
<DBRowSidePanel
|
||||
source={source}
|
||||
rowId={rowId}
|
||||
aliasWith={aliasWith}
|
||||
onClose={_onClose}
|
||||
isNestedPanel={isNestedPanel}
|
||||
breadcrumbPath={breadcrumbPath}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,11 @@ import { useCsvExport } from '@/hooks/useCsvExport';
|
|||
import { useTableMetadata } from '@/hooks/useMetadata';
|
||||
import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery';
|
||||
import { useGroupedPatterns } from '@/hooks/usePatterns';
|
||||
import useRowWhere from '@/hooks/useRowWhere';
|
||||
import useRowWhere, {
|
||||
INTERNAL_ROW_FIELDS,
|
||||
RowWhereResult,
|
||||
WithClause,
|
||||
} from '@/hooks/useRowWhere';
|
||||
import { useSource } from '@/source';
|
||||
import { UNDEFINED_WIDTH } from '@/tableUtils';
|
||||
import { FormatTime } from '@/useFormatTime';
|
||||
|
|
@ -120,7 +124,8 @@ const ACCESSOR_MAP: Record<string, AccessorFn> = {
|
|||
const MAX_SCROLL_FETCH_LINES = 1000;
|
||||
const MAX_CELL_LENGTH = 500;
|
||||
|
||||
const getRowId = (row: Record<string, any>): string => row.__hyperdx_id;
|
||||
const getRowId = (row: Record<string, any>): string =>
|
||||
row[INTERNAL_ROW_FIELDS.ID];
|
||||
|
||||
function retrieveColumnValue(column: string, row: Row): any {
|
||||
const accessor = ACCESSOR_MAP[column] ?? ACCESSOR_MAP.default;
|
||||
|
|
@ -334,7 +339,7 @@ export const RawLogTable = memo(
|
|||
isLoading?: boolean;
|
||||
fetchNextPage?: (options?: FetchNextPageOptions | undefined) => any;
|
||||
onRowDetailsClick: (row: Record<string, any>) => void;
|
||||
generateRowId: (row: Record<string, any>) => string;
|
||||
generateRowId: (row: Record<string, any>) => RowWhereResult;
|
||||
// onPropertySearchClick: (
|
||||
// name: string,
|
||||
// value: string | number | boolean,
|
||||
|
|
@ -360,33 +365,39 @@ export const RawLogTable = memo(
|
|||
onExpandedRowsChange?: (hasExpandedRows: boolean) => void;
|
||||
collapseAllRows?: boolean;
|
||||
showExpandButton?: boolean;
|
||||
renderRowDetails?: (row: Record<string, any>) => React.ReactNode;
|
||||
renderRowDetails?: (row: {
|
||||
id: string;
|
||||
aliasWith?: WithClause[];
|
||||
[key: string]: any;
|
||||
}) => React.ReactNode;
|
||||
enableSorting?: boolean;
|
||||
sortOrder?: SortingState;
|
||||
onSortingChange?: (v: SortingState | null) => void;
|
||||
getRowWhere?: (row: Record<string, any>) => string;
|
||||
getRowWhere?: (row: Record<string, any>) => RowWhereResult;
|
||||
variant?: DBRowTableVariant;
|
||||
}) => {
|
||||
const generateRowMatcher = generateRowId;
|
||||
|
||||
const dedupedRows = useMemo(() => {
|
||||
const lIds = new Set();
|
||||
const returnedRows = dedupRows
|
||||
? rows.filter(l => {
|
||||
const matcher = generateRowMatcher(l);
|
||||
if (lIds.has(matcher)) {
|
||||
const rowWhereResult = generateRowId(l);
|
||||
if (lIds.has(rowWhereResult.where)) {
|
||||
return false;
|
||||
}
|
||||
lIds.add(matcher);
|
||||
lIds.add(rowWhereResult.where);
|
||||
return true;
|
||||
})
|
||||
: rows;
|
||||
|
||||
return returnedRows.map(r => ({
|
||||
...r,
|
||||
__hyperdx_id: generateRowMatcher(r),
|
||||
}));
|
||||
}, [rows, dedupRows, generateRowMatcher]);
|
||||
return returnedRows.map(r => {
|
||||
const rowWhereResult = generateRowId(r);
|
||||
return {
|
||||
...r,
|
||||
[INTERNAL_ROW_FIELDS.ID]: rowWhereResult.where,
|
||||
[INTERNAL_ROW_FIELDS.ALIAS_WITH]: rowWhereResult.aliasWith,
|
||||
};
|
||||
});
|
||||
}, [rows, dedupRows, generateRowId]);
|
||||
|
||||
const _onRowExpandClick = useCallback(
|
||||
(row: Record<string, any>) => {
|
||||
|
|
@ -964,6 +975,7 @@ export const RawLogTable = memo(
|
|||
>
|
||||
{renderRowDetails?.({
|
||||
id: rowId,
|
||||
aliasWith: row.original[INTERNAL_ROW_FIELDS.ALIAS_WITH],
|
||||
...row.original,
|
||||
})}
|
||||
</ExpandedLogRow>
|
||||
|
|
@ -1197,12 +1209,16 @@ function DBSqlRowTableComponent({
|
|||
}: {
|
||||
config: ChartConfigWithDateRange;
|
||||
sourceId?: string;
|
||||
onRowDetailsClick?: (where: string) => void;
|
||||
onRowDetailsClick?: (rowWhere: RowWhereResult) => void;
|
||||
highlightedLineId?: string;
|
||||
queryKeyPrefix?: string;
|
||||
enabled?: boolean;
|
||||
isLive?: boolean;
|
||||
renderRowDetails?: (r: { [key: string]: unknown }) => React.ReactNode;
|
||||
renderRowDetails?: (r: {
|
||||
id: string;
|
||||
aliasWith?: WithClause[];
|
||||
[key: string]: unknown;
|
||||
}) => React.ReactNode;
|
||||
onScroll?: (scrollTop: number) => void;
|
||||
onError?: (error: Error | ClickHouseQueryError) => void;
|
||||
denoiseResults?: boolean;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
|
||||
import {
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from '@hyperdx/common-utils/dist/types';
|
||||
import { SortingState } from '@tanstack/react-table';
|
||||
|
||||
import { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
|
||||
import { useSource } from '@/source';
|
||||
import TabBar from '@/TabBar';
|
||||
import { useLocalStorage } from '@/utils';
|
||||
|
|
@ -63,15 +64,17 @@ export default function DBSqlRowTableWithSideBar({
|
|||
const { data: sourceData } = useSource({ id: sourceId });
|
||||
const [rowId, setRowId] = useQueryState('rowWhere');
|
||||
const [rowSource, setRowSource] = useQueryState('rowSource');
|
||||
const [aliasWith, setAliasWith] = useState<WithClause[]>([]);
|
||||
const { setContextRowId, setContextRowSource } = useNestedPanelState();
|
||||
|
||||
const onOpenSidebar = useCallback(
|
||||
(rowWhere: string) => {
|
||||
setRowId(rowWhere);
|
||||
(rowWhere: RowWhereResult) => {
|
||||
setRowId(rowWhere.where);
|
||||
setAliasWith(rowWhere.aliasWith);
|
||||
setRowSource(sourceId);
|
||||
onSidebarOpen?.(rowWhere);
|
||||
onSidebarOpen?.(rowWhere.where);
|
||||
},
|
||||
[setRowId, setRowSource, sourceId, onSidebarOpen],
|
||||
[setRowId, setAliasWith, setRowSource, sourceId, onSidebarOpen],
|
||||
);
|
||||
|
||||
const onCloseSidebar = useCallback(() => {
|
||||
|
|
@ -91,12 +94,16 @@ export default function DBSqlRowTableWithSideBar({
|
|||
setContextRowSource,
|
||||
]);
|
||||
const renderRowDetails = useCallback(
|
||||
(r: { [key: string]: unknown }) => {
|
||||
(r: { id: string; aliasWith?: WithClause[]; [key: string]: unknown }) => {
|
||||
if (!sourceData) {
|
||||
return <div className="p-3 text-muted">Loading...</div>;
|
||||
}
|
||||
return (
|
||||
<RowOverviewPanelWrapper source={sourceData} rowId={r.id as string} />
|
||||
<RowOverviewPanelWrapper
|
||||
source={sourceData}
|
||||
rowId={r.id}
|
||||
aliasWith={r.aliasWith}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[sourceData],
|
||||
|
|
@ -108,6 +115,7 @@ export default function DBSqlRowTableWithSideBar({
|
|||
<DBRowSidePanel
|
||||
source={sourceData}
|
||||
rowId={rowId ?? undefined}
|
||||
aliasWith={aliasWith}
|
||||
isNestedPanel={isNestedPanel}
|
||||
breadcrumbPath={breadcrumbPath}
|
||||
onClose={onCloseSidebar}
|
||||
|
|
@ -143,9 +151,11 @@ enum InlineTab {
|
|||
function RowOverviewPanelWrapper({
|
||||
source,
|
||||
rowId,
|
||||
aliasWith,
|
||||
}: {
|
||||
source: TSource;
|
||||
rowId: string;
|
||||
aliasWith?: WithClause[];
|
||||
}) {
|
||||
// Use localStorage to persist the selected tab
|
||||
const [activeTab, setActiveTab] = useLocalStorage<InlineTab>(
|
||||
|
|
@ -175,11 +185,15 @@ function RowOverviewPanelWrapper({
|
|||
<div>
|
||||
{activeTab === InlineTab.Overview && (
|
||||
<div className="inline-overview-panel">
|
||||
<RowOverviewPanel source={source} rowId={rowId} />
|
||||
<RowOverviewPanel
|
||||
source={source}
|
||||
rowId={rowId}
|
||||
aliasWith={aliasWith}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === InlineTab.ColumnValues && (
|
||||
<RowDataPanel source={source} rowId={rowId} />
|
||||
<RowDataPanel source={source} rowId={rowId} aliasWith={aliasWith} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import React, { useState } from 'react';
|
||||
import { IconCopy, IconLink, IconTextWrap } from '@tabler/icons-react';
|
||||
|
||||
import { INTERNAL_ROW_FIELDS, RowWhereResult } from '@/hooks/useRowWhere';
|
||||
|
||||
import DBRowTableIconButton from './DBRowTableIconButton';
|
||||
|
||||
import styles from '../../../styles/LogTable.module.scss';
|
||||
|
||||
export interface DBRowTableRowButtonsProps {
|
||||
row: Record<string, any>;
|
||||
getRowWhere: (row: Record<string, any>) => string;
|
||||
getRowWhere: (row: Record<string, any>) => RowWhereResult;
|
||||
sourceId?: string;
|
||||
isWrapped: boolean;
|
||||
onToggleWrap: () => void;
|
||||
|
|
@ -27,7 +29,7 @@ export const DBRowTableRowButtons: React.FC<DBRowTableRowButtonsProps> = ({
|
|||
try {
|
||||
// Filter out internal metadata fields that start with __ or are generated IDs
|
||||
|
||||
const { __hyperdx_id, ...cleanRow } = row;
|
||||
const { [INTERNAL_ROW_FIELDS.ID]: _id, ...cleanRow } = row;
|
||||
|
||||
// Parse JSON string fields to make them proper JSON objects
|
||||
const parsedRow = Object.entries(cleanRow).reduce(
|
||||
|
|
@ -62,10 +64,10 @@ export const DBRowTableRowButtons: React.FC<DBRowTableRowButtonsProps> = ({
|
|||
|
||||
const copyRowUrl = async () => {
|
||||
try {
|
||||
const rowWhere = getRowWhere(row);
|
||||
const rowWhereResult = getRowWhere(row);
|
||||
const currentUrl = new URL(window.location.href);
|
||||
// Add the row identifier as query parameters
|
||||
currentUrl.searchParams.set('rowWhere', rowWhere);
|
||||
currentUrl.searchParams.set('rowWhere', rowWhereResult.where);
|
||||
if (sourceId) {
|
||||
currentUrl.searchParams.set('rowSource', sourceId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { IconPencil } from '@tabler/icons-react';
|
||||
|
||||
import { DBTraceWaterfallChartContainer } from '@/components/DBTraceWaterfallChart';
|
||||
import { WithClause } from '@/hooks/useRowWhere';
|
||||
import { useSource, useUpdateSource } from '@/source';
|
||||
import TabBar from '@/TabBar';
|
||||
|
||||
|
|
@ -95,7 +96,7 @@ export default function DBTracePanel({
|
|||
|
||||
const [eventRowWhere, setEventRowWhere] = useQueryState(
|
||||
'eventRowWhere',
|
||||
parseAsJson<{ id: string; type: string }>(),
|
||||
parseAsJson<{ id: string; type: string; aliasWith: WithClause[] }>(),
|
||||
);
|
||||
|
||||
const {
|
||||
|
|
@ -231,6 +232,7 @@ export default function DBTracePanel({
|
|||
: traceSourceData
|
||||
}
|
||||
rowId={eventRowWhere?.id}
|
||||
aliasWith={eventRowWhere?.aliasWith}
|
||||
/>
|
||||
)}
|
||||
{displayedTab === Tab.Parsed && (
|
||||
|
|
@ -241,6 +243,7 @@ export default function DBTracePanel({
|
|||
: traceSourceData
|
||||
}
|
||||
rowId={eventRowWhere?.id}
|
||||
aliasWith={eventRowWhere?.aliasWith}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
import { ContactSupportText } from '@/components/ContactSupportText';
|
||||
import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery';
|
||||
import useResizable from '@/hooks/useResizable';
|
||||
import useRowWhere from '@/hooks/useRowWhere';
|
||||
import useRowWhere, { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
|
||||
import useWaterfallSearchState from '@/hooks/useWaterfallSearchState';
|
||||
import SearchInputV2 from '@/SearchInputV2';
|
||||
import {
|
||||
|
|
@ -313,7 +313,7 @@ export function useEventsAroundFocus({
|
|||
const meta = beforeSpanData?.meta ?? afterSpanData?.meta;
|
||||
const error = beforeSpanError || afterSpanError;
|
||||
|
||||
const rowWhere = useRowWhere({ meta, aliasMap: alias });
|
||||
const getRowWhere = useRowWhere({ meta, aliasMap: alias });
|
||||
const rows = useMemo(() => {
|
||||
// Sometimes meta has not loaded yet
|
||||
// DO NOT REMOVE, useRowWhere will error if no meta
|
||||
|
|
@ -322,6 +322,11 @@ export function useEventsAroundFocus({
|
|||
...(beforeSpanData?.data ?? []),
|
||||
...(afterSpanData?.data ?? []),
|
||||
].map(cd => {
|
||||
// Omit SpanAttributes, SpanEvents and __hdx_hidden from rowWhere id generation.
|
||||
// SpanAttributes and SpanEvents can be large objects, and __hdx_hidden may be a lucene expression.
|
||||
const rowWhereResult = getRowWhere(
|
||||
omit(cd, ['SpanAttributes', 'SpanEvents', '__hdx_hidden']),
|
||||
);
|
||||
return {
|
||||
// Keep all fields available for display
|
||||
...cd,
|
||||
|
|
@ -329,14 +334,14 @@ export function useEventsAroundFocus({
|
|||
SpanId: cd?.SpanId,
|
||||
__hdx_hidden: cd?.__hdx_hidden,
|
||||
type,
|
||||
// Omit SpanAttributes, SpanEvents and __hdx_hidden from rowWhere id generation.
|
||||
// SpanAttributes and SpanEvents can be large objects, and __hdx_hidden may be a lucene expression.
|
||||
id: rowWhere(
|
||||
omit(cd, ['SpanAttributes', 'SpanEvents', '__hdx_hidden']),
|
||||
),
|
||||
id: rowWhereResult.where,
|
||||
// Don't pass aliasWith for trace waterfall chart - the WHERE clause already uses
|
||||
// raw column expressions (e.g., SpanName='value'), and the aliasMap creates
|
||||
// redundant WITH clauses like (Timestamp) AS Timestamp that interfere with queries.
|
||||
aliasWith: [],
|
||||
};
|
||||
});
|
||||
}, [afterSpanData, beforeSpanData, meta, rowWhere, type]);
|
||||
}, [afterSpanData, beforeSpanData, meta, getRowWhere, type]);
|
||||
|
||||
return {
|
||||
rows,
|
||||
|
|
@ -362,7 +367,11 @@ export function DBTraceWaterfallChartContainer({
|
|||
traceId: string;
|
||||
dateRange: [Date, Date];
|
||||
focusDate: Date;
|
||||
onClick?: (rowWhere: { id: string; type: string }) => void;
|
||||
onClick?: (rowWhere: {
|
||||
id: string;
|
||||
type: string;
|
||||
aliasWith: WithClause[];
|
||||
}) => void;
|
||||
highlightedRowWhere?: string | null;
|
||||
initialRowHighlightHint?: {
|
||||
timestamp: string;
|
||||
|
|
@ -499,6 +508,7 @@ export function DBTraceWaterfallChartContainer({
|
|||
onClick?.({
|
||||
id: rows[initialRowHighlightIndex].id,
|
||||
type: rows[initialRowHighlightIndex].type ?? '',
|
||||
aliasWith: rows[initialRowHighlightIndex].aliasWith,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -510,7 +520,12 @@ export function DBTraceWaterfallChartContainer({
|
|||
// 3. Spans, with multiple root nodes (ex. somehow disjoint traces fe/be)
|
||||
|
||||
// Parse out a DAG of spans
|
||||
type Node = SpanRow & { id: string; parentId: string; children: SpanRow[] };
|
||||
type Node = SpanRow & {
|
||||
id: string;
|
||||
parentId: string;
|
||||
children: SpanRow[];
|
||||
aliasWith: WithClause[];
|
||||
};
|
||||
const validSpanIDs = useMemo(() => {
|
||||
return new Set(
|
||||
traceRowsData // only spans in traces can define valid span ids
|
||||
|
|
@ -653,7 +668,13 @@ export function DBTraceWaterfallChartContainer({
|
|||
const start = startOffset - minOffset;
|
||||
const end = start + tookMs;
|
||||
|
||||
const { Body: _body, ServiceName: serviceName, id, type } = result;
|
||||
const {
|
||||
Body: _body,
|
||||
ServiceName: serviceName,
|
||||
id,
|
||||
type,
|
||||
aliasWith,
|
||||
} = result;
|
||||
let body = `${_body}`;
|
||||
try {
|
||||
body = typeof _body === 'string' ? _body : JSON.stringify(_body);
|
||||
|
|
@ -692,6 +713,7 @@ export function DBTraceWaterfallChartContainer({
|
|||
return {
|
||||
id,
|
||||
type,
|
||||
aliasWith,
|
||||
label: (
|
||||
<div
|
||||
className={`${textColor({ isError, isWarn })} ${
|
||||
|
|
@ -699,7 +721,7 @@ export function DBTraceWaterfallChartContainer({
|
|||
} text-truncate cursor-pointer ps-2 ${styles.traceTimelineLabel}`}
|
||||
role="button"
|
||||
onClick={() => {
|
||||
onClick?.({ id, type: type ?? '' });
|
||||
onClick?.({ id, type: type ?? '', aliasWith });
|
||||
}}
|
||||
>
|
||||
<div className="d-flex align-items-center" style={{ height: 24 }}>
|
||||
|
|
@ -774,6 +796,8 @@ export function DBTraceWaterfallChartContainer({
|
|||
events: [
|
||||
{
|
||||
id,
|
||||
type,
|
||||
aliasWith,
|
||||
start,
|
||||
end,
|
||||
tooltip: `${displayText} ${tookMs >= 0 ? `took ${tookMs.toFixed(4)}ms` : ''} ${status ? `| Status: ${status}` : ''}${!isNaN(startOffset) ? ` | Started at ${formatTime(new Date(startOffset), { format: 'withMs' })}` : ''}`,
|
||||
|
|
@ -921,8 +945,16 @@ export function DBTraceWaterfallChartContainer({
|
|||
onClick={ts => {
|
||||
// onTimeClick(ts + startedAt);
|
||||
}}
|
||||
onEventClick={event => {
|
||||
onClick?.({ id: event.id, type: event.type ?? '' });
|
||||
onEventClick={(event: {
|
||||
id: string;
|
||||
type?: string;
|
||||
aliasWith?: WithClause[];
|
||||
}) => {
|
||||
onClick?.({
|
||||
id: event.id,
|
||||
type: event.type ?? '',
|
||||
aliasWith: event.aliasWith ?? [],
|
||||
});
|
||||
}}
|
||||
cursors={[]}
|
||||
rows={timelineRows}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { useQueryState } from 'nuqs';
|
|||
import { TSource } from '@hyperdx/common-utils/dist/types';
|
||||
import { IconArrowsMaximize, IconChevronRight } from '@tabler/icons-react';
|
||||
|
||||
import { INTERNAL_ROW_FIELDS } from '@/hooks/useRowWhere';
|
||||
|
||||
import styles from '../../styles/LogTable.module.scss';
|
||||
|
||||
// Hook that provides a function to open the sidebar with specific row details
|
||||
|
|
@ -179,7 +181,7 @@ export const createExpandButtonColumn = (
|
|||
highlightedLineId?: string,
|
||||
) => ({
|
||||
id: 'expand-btn',
|
||||
accessorKey: '__hyperdx_id',
|
||||
accessorKey: INTERNAL_ROW_FIELDS.ID,
|
||||
header: () => '',
|
||||
cell: (info: any) => {
|
||||
const rowId = info.getValue() as string;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
SEVERITY_TEXT_COLUMN_ALIAS,
|
||||
TIMESTAMP_COLUMN_ALIAS,
|
||||
} from '@/hooks/usePatterns';
|
||||
import useRowWhere from '@/hooks/useRowWhere';
|
||||
import useRowWhere, { RowWhereResult } from '@/hooks/useRowWhere';
|
||||
import { getFirstTimestampValueExpression } from '@/source';
|
||||
import { useZIndex, ZIndexContext } from '@/zIndex';
|
||||
|
||||
|
|
@ -34,9 +34,8 @@ export default function PatternSidePanel({
|
|||
const contextZIndex = useZIndex();
|
||||
const drawerZIndex = contextZIndex + 100;
|
||||
|
||||
const [selectedRowWhere, setSelectedRowWhere] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedRowWhere, setSelectedRowWhere] =
|
||||
React.useState<RowWhereResult | null>(null);
|
||||
|
||||
const serviceNameExpression = source?.serviceNameExpression || 'Service';
|
||||
|
||||
|
|
@ -81,11 +80,11 @@ export default function PatternSidePanel({
|
|||
|
||||
const handleRowClick = React.useCallback(
|
||||
(row: Record<string, any>) => {
|
||||
const whereClause = getRowWhere({
|
||||
const rowWhereResult = getRowWhere({
|
||||
body: row[PATTERN_COLUMN_ALIAS],
|
||||
ts: row[TIMESTAMP_COLUMN_ALIAS],
|
||||
});
|
||||
setSelectedRowWhere(whereClause);
|
||||
setSelectedRowWhere(rowWhereResult);
|
||||
},
|
||||
[getRowWhere],
|
||||
);
|
||||
|
|
@ -125,7 +124,7 @@ export default function PatternSidePanel({
|
|||
</Card.Section>
|
||||
<RawLogTable
|
||||
rows={pattern.samples}
|
||||
generateRowId={row => row.id}
|
||||
generateRowId={row => ({ where: row.id, aliasWith: [] })}
|
||||
displayedColumns={displayedColumns}
|
||||
columnTypeMap={columnTypeMap}
|
||||
columnNameMap={columnNameMap}
|
||||
|
|
@ -140,7 +139,8 @@ export default function PatternSidePanel({
|
|||
{selectedRowWhere && (
|
||||
<DBRowSidePanel
|
||||
source={source}
|
||||
rowId={selectedRowWhere}
|
||||
rowId={selectedRowWhere.where}
|
||||
aliasWith={selectedRowWhere.aliasWith}
|
||||
onClose={handleCloseRowSidePanel}
|
||||
isNestedPanel={true}
|
||||
breadcrumbPath={[{ label: 'Pattern Overview' }]}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export default function PatternTable({
|
|||
fetchNextPage={() => {}}
|
||||
highlightedLineId={''}
|
||||
columnTypeMap={emptyMap}
|
||||
generateRowId={row => row.id}
|
||||
generateRowId={row => ({ where: row.id, aliasWith: [] })}
|
||||
columnNameMap={{
|
||||
__hdx_pattern_trend: 'Trend',
|
||||
countStr: 'Count',
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import {
|
|||
appendSelectWithPrimaryAndPartitionKey,
|
||||
RawLogTable,
|
||||
} from '@/components/DBRowTable';
|
||||
import { RowWhereResult } from '@/hooks/useRowWhere';
|
||||
|
||||
import * as useChartConfigModule from '../../hooks/useChartConfig';
|
||||
|
||||
const mockRowWhereResult: RowWhereResult = { where: '', aliasWith: [] };
|
||||
|
||||
describe('RawLogTable', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
@ -34,7 +37,7 @@ describe('RawLogTable', () => {
|
|||
dedupRows={false}
|
||||
hasNextPage={false}
|
||||
onRowDetailsClick={() => {}}
|
||||
generateRowId={() => ''}
|
||||
generateRowId={() => mockRowWhereResult}
|
||||
columnTypeMap={new Map()}
|
||||
/>,
|
||||
);
|
||||
|
|
@ -55,7 +58,7 @@ describe('RawLogTable', () => {
|
|||
dedupRows: false,
|
||||
hasNextPage: false,
|
||||
onRowDetailsClick: () => {},
|
||||
generateRowId: () => '',
|
||||
generateRowId: () => mockRowWhereResult,
|
||||
columnTypeMap: new Map(),
|
||||
};
|
||||
it('Should not allow changing sort if disabled', () => {
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ describe('DBTraceWaterfallChartContainer', () => {
|
|||
// Reset mocks before each test
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseRowWhere.mockReturnValue(() => 'row-id');
|
||||
mockUseRowWhere.mockReturnValue(() => ({ where: 'row-id', aliasWith: [] }));
|
||||
MockTimelineChart.latestProps = {};
|
||||
});
|
||||
|
||||
|
|
@ -294,7 +294,7 @@ describe('useEventsAroundFocus', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseRowWhere.mockReturnValue(() => 'row-id');
|
||||
mockUseRowWhere.mockReturnValue(() => ({ where: 'row-id', aliasWith: [] }));
|
||||
});
|
||||
|
||||
const testEventsAroundFocus = (options: {
|
||||
|
|
|
|||
|
|
@ -426,9 +426,10 @@ describe('useRowWhere', () => {
|
|||
const { result } = renderHook(() => useRowWhere({ meta }));
|
||||
|
||||
const row = { id: '123', status: 'active' };
|
||||
const whereClause = result.current(row);
|
||||
const rowWhereResult = result.current(row);
|
||||
|
||||
expect(whereClause).toBe("id='123' AND status='active'");
|
||||
expect(rowWhereResult.where).toBe("id='123' AND status='active'");
|
||||
expect(rowWhereResult.aliasWith).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle aliasMap correctly', () => {
|
||||
|
|
@ -445,9 +446,23 @@ describe('useRowWhere', () => {
|
|||
const { result } = renderHook(() => useRowWhere({ meta, aliasMap }));
|
||||
|
||||
const row = { user_id: '123', user_status: 'active' };
|
||||
const whereClause = result.current(row);
|
||||
const rowWhereResult = result.current(row);
|
||||
|
||||
expect(whereClause).toBe("users.id='123' AND users.status='active'");
|
||||
expect(rowWhereResult.where).toBe(
|
||||
"users.id='123' AND users.status='active'",
|
||||
);
|
||||
expect(rowWhereResult.aliasWith).toEqual([
|
||||
{
|
||||
name: 'user_id',
|
||||
sql: { sql: 'users.id', params: {} },
|
||||
isSubquery: false,
|
||||
},
|
||||
{
|
||||
name: 'user_status',
|
||||
sql: { sql: 'users.status', params: {} },
|
||||
isSubquery: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use column name when alias not found in aliasMap', () => {
|
||||
|
|
@ -464,9 +479,12 @@ describe('useRowWhere', () => {
|
|||
const { result } = renderHook(() => useRowWhere({ meta, aliasMap }));
|
||||
|
||||
const row = { id: '123', status: 'active' };
|
||||
const whereClause = result.current(row);
|
||||
const rowWhereResult = result.current(row);
|
||||
|
||||
expect(whereClause).toBe("users.id='123' AND status='active'");
|
||||
expect(rowWhereResult.where).toBe("users.id='123' AND status='active'");
|
||||
expect(rowWhereResult.aliasWith).toEqual([
|
||||
{ name: 'id', sql: { sql: 'users.id', params: {} }, isSubquery: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle undefined alias values in aliasMap', () => {
|
||||
|
|
@ -483,9 +501,12 @@ describe('useRowWhere', () => {
|
|||
const { result } = renderHook(() => useRowWhere({ meta, aliasMap }));
|
||||
|
||||
const row = { id: '123', status: 'active' };
|
||||
const whereClause = result.current(row);
|
||||
const rowWhereResult = result.current(row);
|
||||
|
||||
expect(whereClause).toBe("users.id='123' AND status='active'");
|
||||
expect(rowWhereResult.where).toBe("users.id='123' AND status='active'");
|
||||
expect(rowWhereResult.aliasWith).toEqual([
|
||||
{ name: 'id', sql: { sql: 'users.id', params: {} }, isSubquery: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should memoize the column map', () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,28 @@ import {
|
|||
|
||||
const MAX_STRING_LENGTH = 512;
|
||||
|
||||
// Internal row field names used by the table component for row tracking
|
||||
export const INTERNAL_ROW_FIELDS = {
|
||||
ID: '__hyperdx_id',
|
||||
ALIAS_WITH: '__hyperdx_alias_with',
|
||||
} as const;
|
||||
|
||||
// Type for WITH clause entries, matching ChartConfig's with property
|
||||
export type WithClause = {
|
||||
name: string;
|
||||
sql: {
|
||||
sql: string;
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
isSubquery: boolean;
|
||||
};
|
||||
|
||||
// Result type for row WHERE clause with alias support
|
||||
export type RowWhereResult = {
|
||||
where: string;
|
||||
aliasWith: WithClause[];
|
||||
};
|
||||
|
||||
type ColumnWithMeta = ColumnMetaType & {
|
||||
valueExpr: string;
|
||||
jsType: JSDataType | null;
|
||||
|
|
@ -111,6 +133,29 @@ export function processRowToWhereClause(
|
|||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an aliasMap to an array of WITH clause entries.
|
||||
* This allows aliases to be properly defined when querying for a specific row.
|
||||
*/
|
||||
export function aliasMapToWithClauses(
|
||||
aliasMap: Record<string, string | undefined> | undefined,
|
||||
): WithClause[] {
|
||||
if (!aliasMap) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(aliasMap)
|
||||
.filter(([, value]) => value != null && value.trim() !== '')
|
||||
.map(([name, value]) => ({
|
||||
name,
|
||||
sql: {
|
||||
sql: value as string,
|
||||
params: {},
|
||||
},
|
||||
isSubquery: false,
|
||||
}));
|
||||
}
|
||||
|
||||
export default function useRowWhere({
|
||||
meta,
|
||||
aliasMap,
|
||||
|
|
@ -126,6 +171,7 @@ export default function useRowWhere({
|
|||
// but if the alias is not found, use the column name as the valueExpr
|
||||
const valueExpr =
|
||||
aliasMap != null ? (aliasMap[c.name] ?? c.name) : c.name;
|
||||
|
||||
return [
|
||||
c.name,
|
||||
{
|
||||
|
|
@ -139,13 +185,22 @@ export default function useRowWhere({
|
|||
[meta, aliasMap],
|
||||
);
|
||||
|
||||
return useCallback(
|
||||
(row: Record<string, any>) => {
|
||||
// Filter out synthetic columns that aren't in the database schema
|
||||
// Memoize the aliasWith array since it only depends on aliasMap
|
||||
const aliasWith = useMemo(() => aliasMapToWithClauses(aliasMap), [aliasMap]);
|
||||
|
||||
const { __hyperdx_id, ...dbRow } = row;
|
||||
return processRowToWhereClause(dbRow, columnMap);
|
||||
return useCallback(
|
||||
(row: Record<string, any>): RowWhereResult => {
|
||||
// Filter out synthetic columns that aren't in the database schema
|
||||
const {
|
||||
[INTERNAL_ROW_FIELDS.ID]: _id,
|
||||
[INTERNAL_ROW_FIELDS.ALIAS_WITH]: _aliasWith,
|
||||
...dbRow
|
||||
} = row;
|
||||
return {
|
||||
where: processRowToWhereClause(dbRow, columnMap),
|
||||
aliasWith,
|
||||
};
|
||||
},
|
||||
[columnMap],
|
||||
[columnMap, aliasWith],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue