diff --git a/.changeset/trace-waterfall-redesign.md b/.changeset/trace-waterfall-redesign.md new file mode 100644 index 0000000000..e5d9f09894 --- /dev/null +++ b/.changeset/trace-waterfall-redesign.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +feat: redesign the trace waterfall — per-service span colors, vertical service color bar, child counts, duration outside the bar with the span body on hover, expand/collapse depth controls. diff --git a/packages/app/src/components/DBTracePanel.tsx b/packages/app/src/components/DBTracePanel.tsx index f5743f38a1..db5668fe5d 100644 --- a/packages/app/src/components/DBTracePanel.tsx +++ b/packages/app/src/components/DBTracePanel.tsx @@ -14,14 +14,13 @@ import { ActionIcon, Box, Button, - Divider, Flex, Group, Stack, Text, Tooltip, } from '@mantine/core'; -import { IconPencil, IconX } from '@tabler/icons-react'; +import { IconX } from '@tabler/icons-react'; import { DBTraceWaterfallChartContainer } from '@/components/DBTraceWaterfallChart'; import { SQLInlineEditorControlled } from '@/components/SQLEditor/SQLInlineEditor'; @@ -266,11 +265,8 @@ export default function DBTracePanel({ } }, [parentSourceData, traceIdSetValue]); - const [showTraceIdInput, setShowTraceIdInput] = useState(false); - - // Tidy the URL: drop a selection that belongs to a different trace. Purely - // cosmetic — the read-time `selectedSpan` gate already prevents a foreign - // selection from rendering; this just keeps the param from lingering. + // Reset highlighted row when trace ID changes + // otherwise we'll show stale span details useEffect(() => { if (eventRowWhere != null && eventRowWhere.traceId !== traceId) { setEventRowWhere(null); @@ -307,50 +303,11 @@ export default function DBTracePanel({ minHeight: 0, }} > - - - - {parentSourceData && - (isLogSource(parentSourceData) || isTraceSource(parentSourceData)) - ? parentSourceData.traceIdExpression - : ''} - : {traceId || 'No trace id found for event'} - - {traceId != null && ( - - )} - - - - {parentSourceData?.kind === SourceKind.Log - ? 'Trace Source' - : 'Correlated Log Source'} - - setIsSourceSchemaPreviewOpen(true)} - isSchemaPreviewEnabled={isSourceSchemaPreviewEnabled( - childSourceData, - )} - /> - setIsSourceSchemaPreviewOpen(false)} - /> - - - {(showTraceIdInput || !traceId) && parentSourceId != null && ( - + {/* Fallback Trace ID Expression editor: only surfaced when no trace id + resolved for the event. The trace id itself now lives in the side + panel header (Copy Trace ID), so it's not duplicated here. */} + {!traceId && parentSourceId != null && ( + Trace ID Expression Save - )} - {/* Inline resizable split view: waterfall (left) + span detail (right) */}
+ + Correlated logs + + setIsSourceSchemaPreviewOpen(true)} + isSchemaPreviewEnabled={isSourceSchemaPreviewEnabled( + childSourceData, + )} + /> + setIsSourceSchemaPreviewOpen(false)} + /> + + } /> )}
diff --git a/packages/app/src/components/DBTraceWaterfallChart.tsx b/packages/app/src/components/DBTraceWaterfallChart.tsx index d907140950..5ab9067317 100644 --- a/packages/app/src/components/DBTraceWaterfallChart.tsx +++ b/packages/app/src/components/DBTraceWaterfallChart.tsx @@ -21,25 +21,33 @@ import { TTraceSource, } from '@hyperdx/common-utils/dist/types'; import { + ActionIcon, Anchor, Box, Center, Chip, Code, Group, - Kbd, + Stack, Text, Tooltip, } from '@mantine/core'; -import { useDisclosure, useElementSize } from '@mantine/hooks'; +import { useElementSize } from '@mantine/hooks'; import { + IconAlertCircleFilled, + IconAlertTriangleFilled, IconChevronDown, IconChevronRight, + IconChevronsDown, + IconChevronsRight, IconLogs, } from '@tabler/icons-react'; import { ContactSupportText } from '@/components/ContactSupportText'; -import SearchInputV2 from '@/components/SearchInput/SearchInputV2'; +import { ErrorCollapse } from '@/components/Error/ErrorCollapse'; +import SearchWhereInput, { + getStoredLanguage, +} from '@/components/SearchInput/SearchWhereInput'; import { TimelineChart, TimelineMinimap, @@ -56,12 +64,12 @@ import { } from '@/source'; import { useFormatTime } from '@/useFormatTime'; import { + CATEGORICAL_PALETTE_TOKENS, + COLORS, getChartColorError, - getChartColorErrorHighlight, getChartColorSuccess, getChartColorSuccessHighlight, getChartColorWarning, - getChartColorWarningHighlight, parseTimestampToMs, } from '@/utils'; import { @@ -97,28 +105,15 @@ type TimestampedRow = { Timestamp: string; }; -function textColor(condition: { isError: boolean; isWarn: boolean }): string { - const { isError, isWarn } = condition; - if (isError) return 'text-danger'; - if (isWarn) return 'text-warning'; - return ''; -} - +// Bar background color. Correlated log rows are always green (success), error +// spans are always red, and every other span takes its per-service color. function barColor(condition: { - isError: boolean; - isWarn: boolean; isHighlighted: boolean; + isError?: boolean; type: string | undefined; + serviceColor?: string; }) { - const { isError, isWarn, isHighlighted, type } = condition; - - if (isError) - return isHighlighted ? getChartColorErrorHighlight() : getChartColorError(); - - if (isWarn) - return isHighlighted - ? getChartColorWarningHighlight() - : getChartColorWarning(); + const { isHighlighted, isError, type, serviceColor } = condition; if (type === SourceKind.Log) { return isHighlighted @@ -126,9 +121,31 @@ function barColor(condition: { : getChartColorSuccess(); } + if (isError) { + return isHighlighted + ? `color-mix(in srgb, ${getChartColorError()} 60%, white)` + : getChartColorError(); + } + + if (serviceColor) { + return isHighlighted + ? `color-mix(in srgb, ${serviceColor} 60%, white)` + : serviceColor; + } + return isHighlighted ? '#A9AFB7' : '#6A7077'; } +// Per-service span palette: the full categorical palette minus the two hues we +// reserve for semantics — green (correlated log rows) and red (error spans) — +// so a service color is never confused with a log or an error. Index-aligned +// with CATEGORICAL_PALETTE_TOKENS, so filtering by token survives reordering. +const SERVICE_COLORS = COLORS.filter( + (_color, i) => + CATEGORICAL_PALETTE_TOKENS[i] !== 'chart-green' && + CATEGORICAL_PALETTE_TOKENS[i] !== 'chart-red', +); + function getTableBody(tableModel: TSource) { if (tableModel?.kind === SourceKind.Trace) { return getSpanEventBody(tableModel) ?? ''; @@ -143,6 +160,7 @@ function getConfig( source: TTraceSource | TLogSource, traceId: string, hiddenRowExpression?: string, + hiddenRowExpressionLanguage: 'lucene' | 'sql' = 'lucene', ) { const alias: Record = { Body: getTableBody(source), @@ -208,7 +226,7 @@ function getConfig( ? [ { valueExpression: hiddenRowExpression, - valueExpressionLanguage: 'lucene' as const, + valueExpressionLanguage: hiddenRowExpressionLanguage, alias: '__hdx_hidden', }, ] @@ -318,18 +336,26 @@ export function useEventsAroundFocus({ traceId, enabled, hiddenRowExpression, + hiddenRowExpressionLanguage = 'lucene', }: { tableSource: TTraceSource | TLogSource; focusDate: Date; dateRange: [Date, Date]; traceId: string; enabled: boolean; - /** A lucene expression that identifies rows to be hidden. Hidden rows will be returned with a `__hdx_hidden: true` column. */ + /** An expression (in `hiddenRowExpressionLanguage`) that identifies rows to be hidden. Hidden rows will be returned with a `__hdx_hidden: true` column. */ hiddenRowExpression?: string; + hiddenRowExpressionLanguage?: 'lucene' | 'sql'; }) { const { config, alias, type } = useMemo( - () => getConfig(tableSource, traceId, hiddenRowExpression), - [tableSource, traceId, hiddenRowExpression], + () => + getConfig( + tableSource, + traceId, + hiddenRowExpression, + hiddenRowExpressionLanguage, + ), + [tableSource, traceId, hiddenRowExpression, hiddenRowExpressionLanguage], ); const { @@ -398,6 +424,39 @@ export function useEventsAroundFocus({ }; } +function useFilteredEventsAroundFocus( + args: Parameters[0], +) { + const filtered = useEventsAroundFocus(args); + + const filterFailed = + args.enabled && !!args.hiddenRowExpression && !!filtered.error; + + const fallback = useEventsAroundFocus({ + ...args, + hiddenRowExpression: undefined, + enabled: filterFailed, + }); + + if (filterFailed) { + return { + rows: fallback.rows, + meta: fallback.meta, + isFetching: filtered.isFetching || fallback.isFetching, + filterError: fallback.error ? undefined : filtered.error, + fatalError: fallback.error, + }; + } + + return { + rows: filtered.rows, + meta: filtered.meta, + isFetching: filtered.isFetching, + filterError: undefined, + fatalError: filtered.error, + }; +} + export function getDescendantIds(node: { id?: string; children?: Array<{ id?: string; children?: any[] }>; @@ -419,14 +478,86 @@ export function getDescendantIds(node: { return ids; } -function CollapseTooltipLabel({ onShown }: { onShown: () => void }) { - useEffect(() => onShown, [onShown]); +/** "Collapse all": every collapsible parent, at every level, becomes collapsed. */ +export function computeCollapseAll( + parentIdsByLevel: Map>, +): Set { + const allParentIds = new Set(); + parentIdsByLevel.forEach(ids => { + ids.forEach(id => allParentIds.add(id)); + }); + return allParentIds; +} + +/** + * "Expand one level": expand the shallowest level that still has any collapsed + * parents. + */ +export function computeExpandOneLevel( + collapsedIds: Set, + parentIdsByLevel: Map>, +): Set { + if (collapsedIds.size === 0) return collapsedIds; + const newSet = new Set(collapsedIds); + // Shallowest first: expanding starts at the top of the tree. + const sortedLevels = [...parentIdsByLevel.keys()].sort((a, b) => a - b); + for (const level of sortedLevels) { + const ids = parentIdsByLevel.get(level)!; + const collapsedAtLevel = [...ids].filter(id => newSet.has(id)); + if (collapsedAtLevel.length > 0) { + collapsedAtLevel.forEach(id => newSet.delete(id)); + break; + } + } + return newSet; +} - return ( - <> - ⌥/Alt + click to collapse children - - ); +export function computeCollapseOneLevel( + collapsedIds: Set, + parentIdsByLevel: Map>, +): Set { + const newSet = new Set(collapsedIds); + // Deepest first: collapsing starts at the bottom of the tree. + const sortedLevels = [...parentIdsByLevel.keys()].sort((a, b) => b - a); + for (const level of sortedLevels) { + const ids = parentIdsByLevel.get(level)!; + const expandedAtLevel = [...ids].filter(id => !newSet.has(id)); + if (expandedAtLevel.length > 0) { + expandedAtLevel.forEach(id => newSet.add(id)); + break; + } + } + return newSet; +} + +export function computeToggleCollapse( + collapsedIds: Set, + id: string, + node: + | { id?: string; children?: Array<{ id?: string; children?: any[] }> } + | undefined, + includeDescendants: boolean, +): Set { + const next = new Set(collapsedIds); + const wasCollapsed = next.has(id); + + if (wasCollapsed) { + next.delete(id); + } else { + next.add(id); + } + + if (includeDescendants && node?.children?.length) { + // Match every descendant to the node's new state. + const descendantIds = getDescendantIds(node); + if (wasCollapsed) { + descendantIds.forEach(descId => next.delete(descId)); + } else { + descendantIds.forEach(descId => next.add(descId)); + } + } + + return next; } // TODO: Optimize with ts lookup tables @@ -440,6 +571,7 @@ export function DBTraceWaterfallChartContainer({ highlightedRowWhere, initialRowHighlightHint, emptyState, + controlsExtra, }: { traceTableSource: TTraceSource; logTableSource: TLogSource | null; @@ -458,31 +590,73 @@ export function DBTraceWaterfallChartContainer({ body: string; }; emptyState?: ReactNode; + /** Extra controls rendered in the waterfall controls bar (e.g. the correlated logs source selector). */ + controlsExtra?: ReactNode; }) { const formatTime = useFormatTime(); const { traceWhere, logWhere, + traceWhereLanguage, + logWhereLanguage, clear: clearFilters, isFilterActive, isFilterExpanded, setIsFilterExpanded, - onSubmit: onSubmitFilters, + onSubmit: submitFilters, } = useWaterfallSearchState({ hasLogSource: !!logTableSource, }); + // Each filter runs in its own language. Defaults come from the URL (shared + // link / reload fidelity), falling back to the stored preference. + const traceFilterLanguage: 'lucene' | 'sql' = + traceWhereLanguage === 'sql' ? 'sql' : 'lucene'; + const logFilterLanguage: 'lucene' | 'sql' = + logWhereLanguage === 'sql' ? 'sql' : 'lucene'; + const { control, handleSubmit, setValue } = useForm({ defaultValues: { traceWhere: traceWhere ?? '', logWhere: logWhere ?? '', + // Prefer each input's URL language so a shared link / reload shows the + // same language the filter runs in; fall back to the stored preference. + traceWhereLanguage: + traceWhereLanguage === 'sql' || traceWhereLanguage === 'lucene' + ? traceWhereLanguage + : (getStoredLanguage() ?? 'lucene'), + logWhereLanguage: + logWhereLanguage === 'sql' || logWhereLanguage === 'lucene' + ? logWhereLanguage + : (getStoredLanguage() ?? 'lucene'), }, }); + const onSubmitFilters = useCallback( + (data: { + traceWhere: string; + logWhere: string; + traceWhereLanguage: string; + logWhereLanguage: string; + }) => { + submitFilters({ + traceWhere: data.traceWhere, + logWhere: data.logWhere, + traceWhereLanguage: data.traceWhereLanguage, + logWhereLanguage: data.logWhereLanguage, + }); + }, + [submitFilters], + ); + const onClearFilters = useCallback(() => { setValue('traceWhere', ''); setValue('logWhere', ''); + // Reset the language toggles too, otherwise stale values get re-serialized + // into the URL on the next submit, undoing the cleared state. + setValue('traceWhereLanguage', getStoredLanguage() ?? 'lucene'); + setValue('logWhereLanguage', getStoredLanguage() ?? 'lucene'); clearFilters(); }, [clearFilters, setValue]); @@ -490,21 +664,24 @@ export function DBTraceWaterfallChartContainer({ rows: traceRowsData, isFetching: traceIsFetching, meta: traceRowsMeta, - error: traceError, - } = useEventsAroundFocus({ + filterError: traceFilterError, + fatalError: traceError, + } = useFilteredEventsAroundFocus({ tableSource: traceTableSource, focusDate, dateRange, traceId, hiddenRowExpression: traceWhere ? `NOT (${traceWhere})` : undefined, + hiddenRowExpressionLanguage: traceFilterLanguage, enabled: true, }); const { rows: logRowsData, isFetching: logIsFetching, meta: logRowsMeta, - error: logError, - } = useEventsAroundFocus({ + filterError: logFilterError, + fatalError: logFatalError, + } = useFilteredEventsAroundFocus({ // search data if logTableModel exist // search invalid date range if no logTableModel(react hook need execute no matter what) tableSource: logTableSource ? logTableSource : traceTableSource, @@ -512,11 +689,20 @@ export function DBTraceWaterfallChartContainer({ dateRange: logTableSource ? dateRange : [dateRange[1], dateRange[0]], // different query to prevent cache traceId, hiddenRowExpression: logWhere ? `NOT (${logWhere})` : undefined, + hiddenRowExpressionLanguage: logFilterLanguage, enabled: logTableSource ? true : false, // disable fire query if logSource is not exist }); const isFetching = traceIsFetching || logIsFetching; - const error = traceError || logError; + // Only a fatal trace failure (bad source / connection) blanks the waterfall. + // A *filter* error on either side is non-fatal: the offending query falls back + // to unfiltered data and the error is surfaced inline next to its input, so a + // bad filter can never hide valid spans or logs. The correlated-log source is + // secondary, so even a fatal log failure only drops logs — never the chart. + const error = traceError; + // Log-side error to surface inline: prefer the filter error (fallback data is + // shown), otherwise the fatal error (no logs could be loaded at all). + const logError = logFilterError ?? logFatalError; const rows: any[] = useMemo(() => { const nextRows: Array<(typeof traceRowsData)[number] & TimestampedRow> = [ @@ -537,6 +723,29 @@ export function DBTraceWaterfallChartContainer({ return nextRows; }, [traceRowsData, logRowsData]); + // Map each distinct span service to a stable color. Sorting the names first + // keeps a service's color stable across renders regardless of row ordering. + const serviceColorMap = useMemo(() => { + const serviceNames = [ + ...new Set( + rows + .filter( + r => + r.ServiceName && + r.type !== SourceKind.Log && + typeof r.ServiceName === 'string', + ) + .map(r => r.ServiceName), + ), + ].sort(); + + const map = new Map(); + serviceNames.forEach((name, i) => { + map.set(name, SERVICE_COLORS[i % SERVICE_COLORS.length]); + }); + return map; + }, [rows]); + const highlightedAttributeValues = useMemo(() => { const visibleTraceRowsData = traceRowsData?.filter( row => !row.__hdx_hidden, @@ -644,7 +853,7 @@ export function DBTraceWaterfallChartContainer({ const [showSpans, setShowSpans] = useState(true); const [showLogs, setShowLogs] = useState(true); - const { nodesMap, flattenedNodes } = useMemo(() => { + const { nodesMap, flattenedNodes, parentIdsByLevel } = useMemo(() => { const rootNodes: Node[] = []; const nodesMap = new Map(); // Maps result.id (or placeholder id) -> Node const spanIdMap = new Map(); // Maps SpanId -> result.id of FIRST node with that SpanId @@ -712,6 +921,20 @@ export function DBTraceWaterfallChartContainer({ } } + // Build a map of level → parent node IDs (nodes that have children) so the + // depth controls can expand/collapse a whole level at a time. + const parentIdsByLevel = new Map>(); + const collectParents = (node: any, level: number) => { + if (node.children?.length > 0 && node.id) { + if (!parentIdsByLevel.has(level)) { + parentIdsByLevel.set(level, new Set()); + } + parentIdsByLevel.get(level)!.add(node.id); + } + node.children?.forEach((child: any) => collectParents(child, level + 1)); + }; + rootNodes.forEach(root => collectParents(root, 0)); + type NodeWithLevel = Node & { level: number }; // flatten the rootnode dag into an array via in-order traversal const traverse = (node: Node, arr: NodeWithLevel[], level = 0) => { @@ -735,40 +958,37 @@ export function DBTraceWaterfallChartContainer({ rootNodes.forEach(rootNode => traverse(rootNode, flattenedNodes)); } - return { nodesMap, flattenedNodes }; + return { nodesMap, flattenedNodes, parentIdsByLevel }; }, [collapsedIds, rows, validSpanIDs]); const toggleCollapse = useCallback( (id: string, event: React.MouseEvent) => { event.stopPropagation(); // prevent collapsing from selecting row + // Alt/Option-click toggles the whole subtree along with the node. + setCollapsedIds(prev => + computeToggleCollapse(prev, id, nodesMap.get(id), event.altKey), + ); + }, + [nodesMap], + ); - setCollapsedIds(prev => { - const newSet = new Set(prev); - const isCollapsed = newSet.has(id); + const expandAll = useCallback(() => { + setCollapsedIds(new Set()); + }, []); - if (isCollapsed) { - newSet.delete(id); - } else { - newSet.add(id); - } + const collapseAll = useCallback(() => { + setCollapsedIds(computeCollapseAll(parentIdsByLevel)); + }, [parentIdsByLevel]); - if (event.altKey) { - const node = nodesMap.get(id); - if (node?.children?.length) { - const descendantIds = getDescendantIds(node); - if (isCollapsed) { - descendantIds.forEach(descId => newSet.delete(descId)); - } else { - descendantIds.forEach(descId => newSet.add(descId)); - } - } - } + const expandOneLevel = useCallback(() => { + setCollapsedIds(prev => computeExpandOneLevel(prev, parentIdsByLevel)); + }, [parentIdsByLevel]); - return newSet; - }); - }, - [nodesMap], - ); + const collapseOneLevel = useCallback(() => { + setCollapsedIds(prev => computeCollapseOneLevel(prev, parentIdsByLevel)); + }, [parentIdsByLevel]); + + const hasCollapsibleNodes = parentIdsByLevel.size > 0; const visibleNodes = useMemo(() => { if (showSpans && showLogs) return flattenedNodes; @@ -811,9 +1031,6 @@ export function DBTraceWaterfallChartContainer({ const minOffset = foundMinOffset === Number.MAX_SAFE_INTEGER ? 0 : foundMinOffset; - const [collapseTooltipShown, { open: setCollapseTooltipShown }] = - useDisclosure(false); - const timelineRows = useMemo( () => visibleNodes.map(result => { @@ -867,13 +1084,20 @@ export function DBTraceWaterfallChartContainer({ const isWarn = result.SeverityText === 'warn'; const isHighlighted = highlightedRowWhere === id; + const barBackgroundColor = + type === SourceKind.Log + ? getChartColorSuccess() + : serviceName + ? (serviceColorMap.get(serviceName) ?? '#6A7077') + : '#6A7077'; + return { id, type, aliasWith, label: (
))} - +
0 ? 1 : 0, + }} + onClick={ + result.children.length > 0 + ? e => { + toggleCollapse(id, e); + } + : undefined } - withArrow > -
0 ? 1 : 0, - }} - onClick={ - result.children.length > 0 - ? e => { - toggleCollapse(id, e); - } - : undefined - } + {collapsedIds.has(id) ? ( + + ) : ( + + )}{' '} +
+ +
+ + {result.children.length > 0 && ( + - {collapsedIds.has(id) ? ( - - ) : ( - - )}{' '} -
-
- - {!isFilterActive && ( - - {result.children.length > 0 - ? `(${result.children.length})` - : ''} + ({result.children.length}) )} + {isError && ( + + )} + {isWarn && !isError && ( + + )} + {type === SourceKind.Log ? ( - {serviceName ? `${serviceName} | ` : ''} - {displayText} + {serviceName && <>{serviceName} } + + {displayText} + @@ -973,10 +1217,12 @@ export function DBTraceWaterfallChartContainer({ tooltip: `${displayText} ${tookMs >= 0 ? `took ${tookMs.toFixed(4)}ms` : ''} ${status ? `| Status: ${status}` : ''}${!isNaN(startOffset) ? ` | Started at ${formatTime(new Date(startOffset), { format: 'withMs' })}` : ''}`, color: 'var(--color-text-inverted)', backgroundColor: barColor({ - isError, - isWarn, isHighlighted, + isError, type, + serviceColor: serviceName + ? serviceColorMap.get(serviceName) + : undefined, }), body: {displayText}, minWidthPx: type === SourceKind.Log ? 10 : 2, @@ -992,13 +1238,11 @@ export function DBTraceWaterfallChartContainer({ visibleNodes, formatTime, highlightedRowWhere, - isFilterActive, minOffset, onClick, + serviceColorMap, showSpanEvents, toggleCollapse, - collapseTooltipShown, - setCollapseTooltipShown, ], ); const initialScrollRowIndex = visibleNodes.findIndex(v => { @@ -1026,50 +1270,134 @@ export function DBTraceWaterfallChartContainer({ )} {isFilterExpanded && (
- - Spans filter - - + + + + Spans filter + + + setValue('traceWhereLanguage', lang, { shouldDirty: true }) + } + lucenePlaceholder='Filter spans ex. StatusCode:"Error"' + sqlPlaceholder="Filter spans ex. StatusCode = 'Error'" + data-testid="trace-search-input" + // The waterfall lives inside an `overflow: hidden` column, which + // clips the SQL editor's autocomplete tooltip. Portal it to the + // document body so suggestions aren't cut off (Lucene mode + // already renders its dropdown in a portal). + parentRef={ + typeof document !== 'undefined' ? document.body : null + } + /> + {traceFilterError && ( + + + + )} + {logTableSource && ( - <> - Logs filter - + + Logs filter + + + setValue('logWhereLanguage', lang, { shouldDirty: true }) + } + lucenePlaceholder='Filter logs ex. SeverityText:"error"' + sqlPlaceholder="Filter logs ex. SeverityText = 'error'" data-testid="log-search-input" + parentRef={ + typeof document !== 'undefined' ? document.body : null + } /> - + {logError && ( + + + + )} + )} - +
)} + {hasCollapsibleNodes && ( + + + + + + + + + + + + + + + + + + + + + + + )} {itemCountString},{' '} @@ -1123,26 +1451,29 @@ export function DBTraceWaterfallChartContainer({ - - setIsFilterExpanded(prev => !prev)} - size="xs" - > - {isFilterExpanded ? 'Hide Filters' : 'Show Filters'}{' '} - {isFilterActive && '(active)'} - - {isFilterActive && ( + + {controlsExtra} + setIsFilterExpanded(prev => !prev)} size="xs" - ms="xs" > - Clear Filters + {isFilterExpanded ? 'Hide Filters' : 'Show Filters'}{' '} + {isFilterActive && '(active)'} - )} - + {isFilterActive && ( + + Clear Filters + + )} + + {!isFetching && !error && highlightedAttributeValues?.length > 0 && ( diff --git a/packages/app/src/components/SearchInput/SearchWhereInput.tsx b/packages/app/src/components/SearchInput/SearchWhereInput.tsx index 296a23cb76..e4f60134b6 100644 --- a/packages/app/src/components/SearchInput/SearchWhereInput.tsx +++ b/packages/app/src/components/SearchInput/SearchWhereInput.tsx @@ -119,6 +119,7 @@ export type SearchWhereInputProps = { * Source id used in various queries */ sourceId?: string; + parentRef?: HTMLElement | null; } & TableConnectionChoice & UseControllerProps; @@ -166,6 +167,7 @@ export default function SearchWhereInput({ dateRange, languageName = `${name}Language`, sourceId, + parentRef, }: SearchWhereInputProps) { const [syntaxRefOpened, { open: openSyntaxRef, close: closeSyntaxRef }] = useDisclosure(false); @@ -240,6 +242,7 @@ export default function SearchWhereInput({ additionalSuggestions={additionalSuggestions} dateRange={dateRange} sourceId={sourceId} + parentRef={parentRef} /> ) : (