diff --git a/.changeset/olive-turkeys-look.md b/.changeset/olive-turkeys-look.md new file mode 100644 index 000000000..ae11edf7e --- /dev/null +++ b/.changeset/olive-turkeys-look.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +refactor: Add ChartContainer component with toolbar diff --git a/packages/app/src/ClickhousePage.tsx b/packages/app/src/ClickhousePage.tsx index 3112bdf0a..210f32f6d 100644 --- a/packages/app/src/ClickhousePage.tsx +++ b/packages/app/src/ClickhousePage.tsx @@ -13,7 +13,6 @@ import { DisplayType } from '@hyperdx/common-utils/dist/types'; import { Box, Button, - Flex, Grid, Group, SegmentedControl, @@ -59,11 +58,9 @@ function InfrastructureTab({ return ( - - - CPU Usage (Cores) - + - - - Memory Usage - + - - - Disk - + - - - S3 Requests - + - - - Network - - - Network activity for the entire machine, not only Clickhouse. - + + + Network + + + Network activity for the entire machine, not only Clickhouse. + + + } config={{ select: [ { @@ -223,6 +222,7 @@ function InfrastructureTab({ connection, dateRange: searchedTimeRange, timestampValueExpression: 'event_time', + displayType: DisplayType.Line, }} onTimeRangeSelect={onTimeRangeSelect} /> @@ -248,32 +248,34 @@ function InsertsTab({ return ( - - - - Insert{' '} - {insertsBy === 'queries' - ? 'Queries' - : insertsBy === 'rows' - ? 'Rows' - : 'Bytes'}{' '} - Per Table - - { - // @ts-ignore - setInsertsBy(value); - }} - data={[ - { label: 'Queries', value: 'queries' }, - { label: 'Rows', value: 'rows' }, - { label: 'Bytes', value: 'bytes' }, - ]} - /> - + + Insert{' '} + {insertsBy === 'queries' + ? 'Queries' + : insertsBy === 'rows' + ? 'Rows' + : 'Bytes'}{' '} + Per Table + + } + toolbarPrefix={[ + { + // @ts-ignore + setInsertsBy(value); + }} + data={[ + { label: 'Queries', value: 'queries' }, + { label: 'Rows', value: 'rows' }, + { label: 'Bytes', value: 'bytes' }, + ]} + />, + ]} config={{ select: insertsBy === 'queries' @@ -306,6 +308,7 @@ function InsertsTab({ where: '', timestampValueExpression: 'event_time', dateRange: searchedTimeRange, + displayType: DisplayType.Line, filters: [ { type: 'sql_ast', @@ -322,11 +325,9 @@ function InsertsTab({ - - - Max Active Parts per Partition - + - - Active Parts Per Partition - - - Recommended to stay under 300, ClickHouse will automatically - throttle inserts after 1,000 parts per partition and stop inserts at - 3,000 parts per partition. - + + Active Parts Per Partition + + + Recommended to stay under 300, ClickHouse will automatically + throttle inserts after 1,000 parts per partition and stop + inserts at 3,000 parts per partition. + + + } config={{ dateRange: searchedTimeRange, select: [ @@ -496,6 +501,29 @@ function ClickhousePage() { ]; }, [latencyFilter]); + const heatmapToolbarItems = useMemo(() => { + if (latencyFilter.latencyMin != null || latencyFilter.latencyMax != null) { + return [ + , + ]; + } + }, [latencyFilter, onSearch, setLatencyFilter]); + return ( @@ -554,75 +582,11 @@ function ClickhousePage() { - {/* - - - - Select P95 Query Latency - - - - { - onTimeRangeSelect(start, end); - }} - /> - - */} - - - Query Latency - - {latencyFilter.latencyMin != null || - latencyFilter.latencyMax != null ? ( - - ) : null} - - - Query Count by Table - - { onTimeRangeSelect(start, end); @@ -697,10 +659,8 @@ function ClickhousePage() { - - Most Time Consuming Query Patterns - (undefined); - // Transform the queried config to match what will be queried by the - // child components, so that the MV Optimization indicator is accurate. - const configForMVOptimizationIndicator = useMemo(() => { - if (!queriedConfig) return undefined; - - if ( - queriedConfig.displayType === DisplayType.Line || - queriedConfig.displayType === DisplayType.StackedBar - ) { - return convertToTimeChartConfig(queriedConfig); - } else if (queriedConfig.displayType === DisplayType.Number) { - return convertToNumberChartConfig(queriedConfig); - } else if (queriedConfig.displayType === DisplayType.Table) { - return convertToTableChartConfig(queriedConfig); - } - - return queriedConfig; - }, [queriedConfig]); - const { data: source } = useSource({ id: chart.config.source, }); @@ -250,10 +224,96 @@ const Tile = forwardRef( return tooltip; }, [alert]); + const hoverToolbar = useMemo(() => { + return hovered ? ( + e.stopPropagation()} + key="hover-toolbar" + > + {(chart.config.displayType === DisplayType.Line || + chart.config.displayType === DisplayType.StackedBar) && ( + +} + mr={4} + > + + + + + )} + + + + + + ) : ( + + ); + }, [ + alert, + alertIndicatorColor, + alertTooltip, + chart.config.displayType, + chart.id, + hovered, + onDeleteClick, + onDuplicateClick, + onEditClick, + ]); + + const title = useMemo( + () => ( + + {chart.config.name} + + ), + [chart.config.name], + ); + return (
-
- - {chart.config.name} - - - {hovered ? ( - e.stopPropagation()}> - {(chart.config.displayType === DisplayType.Line || - chart.config.displayType === DisplayType.StackedBar) && ( - +} - mr={4} - > - - - - - )} - - - - - - ) : ( - - )} - {source?.materializedViews?.length && queriedConfig && ( - e.stopPropagation()}> - - - )} - -
+ + +
e.stopPropagation()} > buildTableRowSearchUrl({ @@ -390,10 +381,18 @@ const Tile = forwardRef( )} {queriedConfig?.displayType === DisplayType.Number && ( - + )} {queriedConfig?.displayType === DisplayType.Markdown && ( - + )} {queriedConfig?.displayType === DisplayType.Search && ( @@ -1899,6 +1900,7 @@ function DBSearchPage() { config={histogramTimeChartConfig} enabled={isReady} showDisplaySwitcher={false} + showMVOptimizationIndicator={false} queryKeyPrefix={QUERY_KEY_PREFIX} onTimeRangeSelect={handleTimeRangeSelect} enableParallelQueries diff --git a/packages/app/src/HDXMarkdownChart.tsx b/packages/app/src/HDXMarkdownChart.tsx index f0369284a..971d6f50b 100644 --- a/packages/app/src/HDXMarkdownChart.tsx +++ b/packages/app/src/HDXMarkdownChart.tsx @@ -1,18 +1,30 @@ import { memo } from 'react'; import ReactMarkdown from 'react-markdown'; +import ChartContainer from './components/charts/ChartContainer'; + const HDXMarkdownChart = memo( ({ config: { markdown }, + title, + toolbarItems, }: { + title?: React.ReactNode; + toolbarItems?: React.ReactNode[]; config: { markdown?: string; }; }) => { return ( -
- {markdown ?? ''} -
+ +
+ {markdown ?? ''} +
+
); }, ); diff --git a/packages/app/src/KubernetesDashboardPage.tsx b/packages/app/src/KubernetesDashboardPage.tsx index 4743c278e..c958e4294 100644 --- a/packages/app/src/KubernetesDashboardPage.tsx +++ b/packages/app/src/KubernetesDashboardPage.tsx @@ -344,7 +344,7 @@ export const InfraPodsStatusTable = ({ return ( - + Pods - + Nodes @@ -815,7 +815,7 @@ const NamespacesTable = ({ return ( - + Namespaces @@ -1297,12 +1297,10 @@ function KubernetesDashboardPage() { - - CPU Usage - {metricSource && ( - - Memory Usage - {metricSource && ( - + Latest Kubernetes Warning Events {/* @@ -1464,12 +1460,10 @@ function KubernetesDashboardPage() { - - CPU Usage - {metricSource && ( - - Memory Usage - {metricSource && ( - - CPU Usage - {metricSource && ( - - Memory Usage - {metricSource && ( - + Latest Namespace Logs & Spans @@ -359,11 +359,9 @@ export default function NamespaceDetailsSidePanel({ /> - - CPU Usage by Pod - - - Memory Usage by Pod - - + Latest Node Logs & Spans @@ -375,11 +368,9 @@ export default function NodeDetailsSidePanel({ /> - - CPU Usage by Pod - - - Memory Usage by Pod - - + Latest Pod Logs & Spans @@ -368,11 +368,9 @@ export default function PodDetailsSidePanel({ /> - - CPU Usage - - - Memory Usage - - + Latest Pod Events diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index fc13cd7ac..4c15c22a9 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import dynamic from 'next/dynamic'; -import cx from 'classnames'; import { pick } from 'lodash'; import { parseAsString, @@ -22,7 +21,6 @@ import { TSource, } from '@hyperdx/common-utils/dist/types'; import { - ActionIcon, Box, Button, Grid, @@ -72,6 +70,7 @@ import { import { useSource, useSources } from '@/source'; import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery'; +import DisplaySwitcher from './components/charts/DisplaySwitcher'; import usePresetDashboardFilters from './hooks/usePresetDashboardFilters'; import { IS_LOCAL_MODE } from './config'; import DashboardFilters from './DashboardFilters'; @@ -214,43 +213,34 @@ export function EndpointLatencyChart({ 'line' | 'histogram' >('line'); + const displaySwitcher = ( + , + }, + { + value: 'histogram', + label: 'Display as Histogram', + icon: , + }, + ]} + /> + ); + return ( - - Request Latency -
- - setLatencyChartType('line')} - > - - - - - - setLatencyChartType('histogram')} - > - - - -
-
{source && expressions && (latencyChartType === 'line' ? ( ) : ( - - Request Error Rate - - {source && requestErrorRateConfig && ( , + ]} sourceId={source.id} hiddenSeries={['total_requests', 'error_requests']} config={requestErrorRateConfig} @@ -569,11 +562,9 @@ function HttpTab({ - - Request Throughput - {source && expressions && ( - - 20 Top Most Time Consuming Endpoints - - {source && expressions && ( - - - Top 20{' '} - {topEndpointsChartType === 'time' - ? 'Most Time Consuming' - : 'Highest Error Rate'} - - { - if (value === 'time' || value === 'error') { - setTopEndpointsChartType(value); - } - }} - data={[ - { label: 'Sort by Time', value: 'time' }, - { label: 'Sort by Errors', value: 'error' }, - ]} - /> - {source && expressions && ( + Top 20{' '} + {topEndpointsChartType === 'time' + ? 'Most Time Consuming' + : 'Highest Error Rate'} + + } + toolbarSuffix={[ + { + if (value === 'time' || value === 'error') { + setTopEndpointsChartType(value); + } + }} + data={[ + { label: 'Sort by Time', value: 'time' }, + { label: 'Sort by Errors', value: 'error' }, + ]} + />, + ]} getRowSearchLink={getRowSearchLink} hiddenColumns={[ 'total_count', @@ -1098,15 +1089,33 @@ function DatabaseTab({ } satisfies ChartConfigWithDateRange; }, [appliedConfig, expressions, searchedTimeRange, source]); + const displaySwitcher = ( + , + value: 'list', + }, + { + label: 'Show as Table', + icon: , + value: 'table', + }, + ]} + /> + ); + return ( - - Total Time Consumed per Query - {source && totalTimePerQueryConfig && ( - - Throughput per Query - {source && totalThroughputPerQueryConfig && ( - - Top 20 Most Time Consuming Queries - - - - - - - - {source && expressions && (chartType === 'list' ? ( ) : ( - - Error Events per Service - {source && expressions && ( ) : undefined} {queryReady && queriedConfig != null && activeTab === 'table' && ( -
+
@@ -1267,14 +1264,12 @@ export default function EditTimeChartForm({ } onSortingChange={onTableSortingChange} sort={tableSortState} + showMVOptimizationIndicator={false} />
)} {queryReady && dbTimeChartConfig != null && activeTab === 'time' && ( -
+
)} {queryReady && queriedConfig != null && activeTab === 'number' && ( -
- +
+
)} {queryReady && diff --git a/packages/app/src/components/DBHeatmapChart.tsx b/packages/app/src/components/DBHeatmapChart.tsx index 8aa2976ae..29c0db318 100644 --- a/packages/app/src/components/DBHeatmapChart.tsx +++ b/packages/app/src/components/DBHeatmapChart.tsx @@ -9,15 +9,7 @@ import { } from '@hyperdx/common-utils/dist/clickhouse'; import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { DisplayType } from '@hyperdx/common-utils/dist/types'; -import { - Button, - Code, - Divider, - Group, - Modal, - Paper, - Text, -} from '@mantine/core'; +import { Box, Button, Code, Divider, Group, Modal, Text } from '@mantine/core'; import { useDisclosure, useElementSize } from '@mantine/hooks'; import { IconArrowsDiagonal } from '@tabler/icons-react'; @@ -31,6 +23,7 @@ import { NumberFormat } from '@/types'; import { FormatTime } from '@/useFormatTime'; import { formatNumber } from '@/utils'; +import ChartContainer from './charts/ChartContainer'; import { SQLPreview } from './ChartSQLPreview'; type Mode2DataArray = [number[], number[], number[]]; @@ -294,10 +287,16 @@ function HeatmapContainer({ config, enabled = true, onFilter, + title, + toolbarPrefix, + toolbarSuffix, }: { config: HeatmapChartConfig; enabled?: boolean; onFilter?: (xMin: number, xMax: number, yMin: number, yMax: number) => void; + title?: React.ReactNode; + toolbarPrefix?: React.ReactNode[]; + toolbarSuffix?: React.ReactNode[]; }) { const dateRange = config.dateRange; const granularity = convertDateRangeToGranularityString(dateRange, 245); @@ -478,82 +477,90 @@ function HeatmapContainer({ } } - if (isLoading || isMinMaxLoading) { - return ( - - + const toolbarItemsMemo = useMemo(() => { + const allToolbarItems = []; + + if (toolbarPrefix && toolbarPrefix.length > 0) { + allToolbarItems.push(...toolbarPrefix); + } + + if (toolbarSuffix && toolbarSuffix.length > 0) { + allToolbarItems.push(...toolbarSuffix); + } + + return allToolbarItems; + }, [toolbarPrefix, toolbarSuffix]); + + const _error = error || minMaxError; + + return ( + + {isLoading || isMinMaxLoading ? ( + Loading... - - ); - } - if (error || minMaxError) { - const _error: Error = error || minMaxError!; - return ( - - - Error loading chart, please check your query or try again later. - - - errorModalControls.close()} - title="Error Details" - > - - - Error Message: - - - {_error.message} - - {_error instanceof ClickHouseQueryError && ( - <> - - Sent Query: - - - - )} - - - - ); - } - - if (time.length < 2 || generatedTsBuckets?.length < 2) { - return ( - - + ) : _error ? ( + + + Error loading chart, please check your query or try again later. + + + errorModalControls.close()} + title="Error Details" + > + + + Error Message: + + + {_error.message} + + {_error instanceof ClickHouseQueryError && ( + <> + + Sent Query: + + + + )} + + + + ) : time.length < 2 || generatedTsBuckets?.length < 2 ? ( + Not enough data points to render heatmap. Try expanding your search criteria. - - ); - } - - return ( - + ) : ( + + )} + ); } diff --git a/packages/app/src/components/DBHistogramChart.tsx b/packages/app/src/components/DBHistogramChart.tsx index b4952a6a8..40bb11427 100644 --- a/packages/app/src/components/DBHistogramChart.tsx +++ b/packages/app/src/components/DBHistogramChart.tsx @@ -15,9 +15,11 @@ import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { Box, Code, Text } from '@mantine/core'; import { useQueriedChartConfig } from '@/hooks/useChartConfig'; +import { useSource } from '@/source'; import { omit } from '@/utils'; -import { generateSearchUrl } from '@/utils'; +import ChartContainer from './charts/ChartContainer'; +import MVOptimizationIndicator from './MaterializedViews/MVOptimizationIndicator'; import { SQLPreview } from './ChartSQLPreview'; function HistogramChart({ @@ -190,11 +192,19 @@ export default function DBHistogramChart({ onSettled, queryKeyPrefix, enabled, + title, + toolbarPrefix, + toolbarSuffix, + showMVOptimizationIndicator = true, }: { config: ChartConfigWithDateRange; onSettled?: () => void; queryKeyPrefix?: string; enabled?: boolean; + title?: React.ReactNode; + toolbarPrefix?: React.ReactNode[]; + toolbarSuffix?: React.ReactNode[]; + showMVOptimizationIndicator?: boolean; }) { const queriedConfig = omit(config, ['granularity']); const { data, isLoading, isError, error } = useQueriedChartConfig( @@ -206,66 +216,82 @@ export default function DBHistogramChart({ }, ); - const genSearchUrl = () => {}; - // Don't ask me why... const buckets = data?.data?.[0]?.data; - return isLoading ? ( -
- Loading Chart Data... -
- ) : isError ? ( -
- - Error loading chart, please check your query or try again later. - - - - Error Message: - - - {error.message} - - {error instanceof ClickHouseQueryError && ( - <> + const { data: source } = useSource({ id: config.source }); + + const toolbarItemsMemo = useMemo(() => { + const allToolbarItems = []; + + if (toolbarPrefix && toolbarPrefix.length > 0) { + allToolbarItems.push(...toolbarPrefix); + } + + if (source && showMVOptimizationIndicator) { + allToolbarItems.push( + , + ); + } + + if (toolbarSuffix && toolbarSuffix.length > 0) { + allToolbarItems.push(...toolbarSuffix); + } + + return allToolbarItems; + }, [ + config, + toolbarPrefix, + toolbarSuffix, + source, + showMVOptimizationIndicator, + ]); + + return ( + + {isLoading ? ( +
+ Loading Chart Data... +
+ ) : isError ? ( +
+ + Error loading chart, please check your query or try again later. + + - Sent Query: + Error Message: - - - )} - -
- ) : data?.data.length === 0 ? ( -
- No data found within time range. -
- ) : ( -
-
+ + {error.message} + + {error instanceof ClickHouseQueryError && ( + <> + + Sent Query: + + + + )} + +
+ ) : data?.data.length === 0 ? ( +
+ No data found within time range. +
+ ) : ( -
-
+ )} + ); } diff --git a/packages/app/src/components/DBInfraPanel.tsx b/packages/app/src/components/DBInfraPanel.tsx index 7d074cc44..23478d4bd 100644 --- a/packages/app/src/components/DBInfraPanel.tsx +++ b/packages/app/src/components/DBInfraPanel.tsx @@ -98,12 +98,10 @@ const InfraSubpanelGroup = ({ - - - CPU Usage (%) - - + + - - - Memory Used - - + + - - - Disk Available - - + + - + Pod Timeline diff --git a/packages/app/src/components/DBListBarChart.tsx b/packages/app/src/components/DBListBarChart.tsx index 36865fa7e..4f9669562 100644 --- a/packages/app/src/components/DBListBarChart.tsx +++ b/packages/app/src/components/DBListBarChart.tsx @@ -6,10 +6,13 @@ import type { FloatingPosition } from '@mantine/core'; import { Box, Code, Flex, HoverCard, Text } from '@mantine/core'; import { useQueriedChartConfig } from '@/hooks/useChartConfig'; +import { useSource } from '@/source'; import type { NumberFormat } from '@/types'; import { omit } from '@/utils'; import { formatNumber, semanticKeyedColor } from '@/utils'; +import ChartContainer from './charts/ChartContainer'; +import MVOptimizationIndicator from './MaterializedViews/MVOptimizationIndicator'; import { SQLPreview } from './ChartSQLPreview'; function ListItem({ @@ -176,6 +179,9 @@ export default function DBListBarChart({ valueColumn, groupColumn, hiddenSeries = [], + title, + toolbarItems, + showMVOptimizationIndicator = true, }: { config: ChartConfigWithDateRange; onSettled?: () => void; @@ -186,6 +192,9 @@ export default function DBListBarChart({ valueColumn: string; groupColumn: string; hiddenSeries?: string[]; + title?: React.ReactNode; + toolbarItems?: React.ReactNode[]; + showMVOptimizationIndicator?: boolean; }) { const queriedConfig = omit(config, ['granularity']); const { data, isLoading, isError, error } = useQueriedChartConfig( @@ -197,6 +206,8 @@ export default function DBListBarChart({ }, ); + const { data: source } = useSource({ id: config.source }); + const columns = useMemo(() => { const rows = data?.data ?? []; if (rows.length === 0) { @@ -212,49 +223,74 @@ export default function DBListBarChart({ })); }, [config.numberFormat, data, hiddenSeries]); - return isLoading && !data ? ( -
- Loading Chart Data... -
- ) : isError ? ( -
- - Error loading chart, please check your query or try again later. - - - - Error Message: - - - {error.message} - - {error instanceof ClickHouseQueryError && ( - <> + const toolbarItemsMemo = useMemo(() => { + const allToolbarItems = []; + + if (source && showMVOptimizationIndicator) { + allToolbarItems.push( + , + ); + } + + if (toolbarItems && toolbarItems.length > 0) { + allToolbarItems.push(...toolbarItems); + } + + return allToolbarItems; + }, [config, source, toolbarItems, showMVOptimizationIndicator]); + + return ( + + {isLoading && !data ? ( +
+ Loading Chart Data... +
+ ) : isError ? ( +
+ + Error loading chart, please check your query or try again later. + + - Sent Query: + Error Message: - - - )} - -
- ) : data?.data.length === 0 ? ( -
- No data found within time range. -
- ) : ( - + + {error.message} + + {error instanceof ClickHouseQueryError && ( + <> + + Sent Query: + + + + )} +
+
+ ) : data?.data.length === 0 ? ( +
+ No data found within time range. +
+ ) : ( + + )} + ); } diff --git a/packages/app/src/components/DBNumberChart.tsx b/packages/app/src/components/DBNumberChart.tsx index dfa840278..53d587715 100644 --- a/packages/app/src/components/DBNumberChart.tsx +++ b/packages/app/src/components/DBNumberChart.tsx @@ -5,18 +5,29 @@ import { Box, Code, Flex, Text } from '@mantine/core'; import { convertToNumberChartConfig } from '@/ChartUtils'; import { useQueriedChartConfig } from '@/hooks/useChartConfig'; +import { useSource } from '@/source'; import { formatNumber } from '@/utils'; +import ChartContainer from './charts/ChartContainer'; +import MVOptimizationIndicator from './MaterializedViews/MVOptimizationIndicator'; import { SQLPreview } from './ChartSQLPreview'; export default function DBNumberChart({ config, enabled = true, queryKeyPrefix, + title, + toolbarPrefix, + toolbarSuffix, + showMVOptimizationIndicator = true, }: { config: ChartConfigWithDateRange; queryKeyPrefix?: string; enabled?: boolean; + title?: React.ReactNode; + toolbarPrefix?: React.ReactNode[]; + toolbarSuffix?: React.ReactNode[]; + showMVOptimizationIndicator?: boolean; }) { const queriedConfig = useMemo( () => convertToNumberChartConfig(config), @@ -37,44 +48,81 @@ export default function DBNumberChart({ config.numberFormat, ); - return isLoading && !data ? ( -
- Loading Chart Data... -
- ) : isError ? ( -
- - Error loading chart, please check your query or try again later. - - - - Error Message: - - - {error.message} - - {error instanceof ClickHouseQueryError && ( - <> + const { data: source } = useSource({ id: config.source }); + + const toolbarItemsMemo = useMemo(() => { + const allToolbarItems = []; + + if (toolbarPrefix && toolbarPrefix.length > 0) { + allToolbarItems.push(...toolbarPrefix); + } + + if (source && showMVOptimizationIndicator) { + allToolbarItems.push( + , + ); + } + + if (toolbarSuffix && toolbarSuffix.length > 0) { + allToolbarItems.push(...toolbarSuffix); + } + + return allToolbarItems; + }, [ + config, + toolbarPrefix, + toolbarSuffix, + source, + showMVOptimizationIndicator, + ]); + + return ( + + {isLoading && !data ? ( +
+ Loading Chart Data... +
+ ) : isError ? ( +
+ + Error loading chart, please check your query or try again later. + + - Sent Query: + Error Message: - - - )} - -
- ) : data?.data.length === 0 ? ( -
- No data found within time range. -
- ) : ( - - {number ?? 'N/A'} - + + {error.message} + + {error instanceof ClickHouseQueryError && ( + <> + + Sent Query: + + + + )} +
+
+ ) : data?.data.length === 0 ? ( +
+ No data found within time range. +
+ ) : ( + + {number ?? 'N/A'} + + )} + ); } diff --git a/packages/app/src/components/DBTableChart.tsx b/packages/app/src/components/DBTableChart.tsx index 64874504f..de97ca235 100644 --- a/packages/app/src/components/DBTableChart.tsx +++ b/packages/app/src/components/DBTableChart.tsx @@ -10,8 +10,11 @@ import { SortingState } from '@tanstack/react-table'; import { convertToTableChartConfig } from '@/ChartUtils'; import { Table } from '@/HDXMultiSeriesTableChart'; import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery'; +import { useSource } from '@/source'; import { useIntersectionObserver } from '@/utils'; +import ChartContainer from './charts/ChartContainer'; +import MVOptimizationIndicator from './MaterializedViews/MVOptimizationIndicator'; import { SQLPreview } from './ChartSQLPreview'; // TODO: Support clicking in to view matched events @@ -23,6 +26,10 @@ export default function DBTableChart({ onSortingChange, sort: controlledSort, hiddenColumns = [], + title, + toolbarPrefix, + toolbarSuffix, + showMVOptimizationIndicator = true, }: { config: ChartConfigWithOptTimestamp; getRowSearchLink?: (row: any) => string | null; @@ -31,9 +38,15 @@ export default function DBTableChart({ onSortingChange?: (sort: SortingState) => void; sort?: SortingState; hiddenColumns?: string[]; + title?: React.ReactNode; + toolbarPrefix?: React.ReactNode[]; + toolbarSuffix?: React.ReactNode[]; + showMVOptimizationIndicator?: boolean; }) { const [sort, setSort] = useState([]); + const { data: source } = useSource({ id: config.source }); + const effectiveSort = useMemo( () => controlledSort || sort, [controlledSort, sort], @@ -115,55 +128,90 @@ export default function DBTableChart({ hiddenColumns, ]); - return isLoading && !data ? ( -
- Loading Chart Data... -
- ) : isError && error ? ( -
- - Error loading chart, please check your query or try again later. - - - - Error Message: - - - {error.message} - - {error instanceof ClickHouseQueryError && ( - <> + const toolbarItemsMemo = useMemo(() => { + const allToolbarItems = []; + + if (toolbarPrefix && toolbarPrefix.length > 0) { + allToolbarItems.push(...toolbarPrefix); + } + + if (source && showMVOptimizationIndicator) { + allToolbarItems.push( + , + ); + } + + if (toolbarSuffix && toolbarSuffix.length > 0) { + allToolbarItems.push(...toolbarSuffix); + } + + return allToolbarItems; + }, [ + config, + toolbarPrefix, + toolbarSuffix, + source, + showMVOptimizationIndicator, + ]); + + return ( + + {isLoading && !data ? ( +
+ Loading Chart Data... +
+ ) : isError && error ? ( +
+ + Error loading chart, please check your query or try again later. + + - Sent Query: + Error Message: - - - )} - -
- ) : data?.data.length === 0 ? ( -
- No data found within time range. -
- ) : ( - - Loading... - - ) - } - /> + + {error.message} + + {error instanceof ClickHouseQueryError && ( + <> + + Sent Query: + + + + )} + + + ) : data?.data.length === 0 ? ( +
+ No data found within time range. +
+ ) : ( +
+ Loading... + + ) + } + /> + )} + ); } diff --git a/packages/app/src/components/DBTimeChart.tsx b/packages/app/src/components/DBTimeChart.tsx index 6910f1760..2a884fe07 100644 --- a/packages/app/src/components/DBTimeChart.tsx +++ b/packages/app/src/components/DBTimeChart.tsx @@ -1,6 +1,5 @@ -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; -import cx from 'classnames'; import { add, differenceInSeconds } from 'date-fns'; import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse'; import { @@ -8,7 +7,6 @@ import { DisplayType, } from '@hyperdx/common-utils/dist/types'; import { - ActionIcon, Button, Code, Group, @@ -24,6 +22,7 @@ import { IconArrowsDiagonal, IconChartBar, IconChartLine, + IconClock, IconSearch, } from '@tabler/icons-react'; @@ -44,6 +43,9 @@ import { MemoChart } from '@/HDXMultiSeriesTimeChart'; import { useQueriedChartConfig } from '@/hooks/useChartConfig'; import { useSource } from '@/source'; +import ChartContainer from './charts/ChartContainer'; +import DisplaySwitcher from './charts/DisplaySwitcher'; +import MVOptimizationIndicator from './MaterializedViews/MVOptimizationIndicator'; import { SQLPreview } from './ChartSQLPreview'; type ActiveClickPayload = { @@ -214,6 +216,10 @@ type DBTimeChartComponentProps = { sourceId?: string; /** Names of series that should not be shown in the chart */ hiddenSeries?: string[]; + title?: React.ReactNode; + toolbarPrefix?: React.ReactNode[]; + toolbarSuffix?: React.ReactNode[]; + showMVOptimizationIndicator?: boolean; }; function DBTimeChartComponent({ @@ -231,6 +237,10 @@ function DBTimeChartComponent({ showLegend = true, sourceId, hiddenSeries, + title, + toolbarPrefix, + toolbarSuffix, + showMVOptimizationIndicator = true, }: DBTimeChartComponentProps) { const [isErrorExpanded, errorExpansion] = useDisclosure(false); @@ -315,7 +325,7 @@ function DBTimeChartComponent({ !data?.isComplete || (config.compareToPreviousPeriod && !previousPeriodData?.isComplete) || isPlaceholderData; - const { data: source } = useSource({ id: sourceId }); + const { data: source } = useSource({ id: sourceId || config.source }); const { graphResults, @@ -379,13 +389,16 @@ function DBTimeChartComponent({ } }, [displayTypeLocal, displayTypeProp, setDisplayType]); - const handleSetDisplayType = (type: DisplayType) => { - if (setDisplayType) { - setDisplayType(type); - } else { - setDisplayTypeLocal(type); - } - }; + const handleSetDisplayType = useCallback( + (type: DisplayType) => { + if (setDisplayType) { + setDisplayType(type); + } else { + setDisplayTypeLocal(type); + } + }, + [setDisplayType], + ); useEffect(() => { if (config.compareToPreviousPeriod) { @@ -535,166 +548,145 @@ function DBTimeChartComponent({ ], ); - return isLoading && !data ? ( -
- Loading Chart Data... -
- ) : isError ? ( -
- - Error loading chart, please check your query or try again later. - - - errorExpansion.close()} - title="Error Details" - > - - - Error Message: + const toolbarItemsMemo = useMemo(() => { + const allToolbarItems = []; + + if (toolbarPrefix && toolbarPrefix.length > 0) { + allToolbarItems.push(...toolbarPrefix); + } + + if (source && showMVOptimizationIndicator) { + allToolbarItems.push( + , + ); + } + + if (showDisplaySwitcher) { + allToolbarItems.push( + , + }, + { + value: DisplayType.StackedBar, + label: config.compareToPreviousPeriod + ? 'Bar Chart Unavailable When Comparing to Previous Period' + : 'Display as Bar Chart', + icon: , + disabled: config.compareToPreviousPeriod, + }, + ]} + />, + ); + } + + if (toolbarSuffix && toolbarSuffix.length > 0) { + allToolbarItems.push(...toolbarSuffix); + } + + return allToolbarItems; + }, [ + config, + displayType, + handleSetDisplayType, + showDisplaySwitcher, + source, + toolbarPrefix, + toolbarSuffix, + showMVOptimizationIndicator, + ]); + + return ( + + {isLoading && !data ? ( +
+ Loading Chart Data... +
+ ) : isError ? ( +
+ + Error loading chart, please check your query or try again later. - errorExpansion.open()} > - {error.message} - - {error instanceof ClickHouseQueryError && ( - <> - - Sent Query: - - - - )} - - -
- ) : graphResults.length === 0 ? ( -
- No data found within time range. -
- ) : ( -
-
- setActiveClickPayload(undefined)} - /> - {/* {totalGroups > groupKeys.length ? ( -
- - Only top{' '} - {groupKeys.length} groups shown - -
- ) : null*/} - {showDisplaySwitcher && ( -
+ + See Error Details + + + errorExpansion.close()} + title="Error Details" > - - handleSetDisplayType(DisplayType.Line)} - > - - - - - - handleSetDisplayType(DisplayType.StackedBar)} + + + Error Message: + + - - - -
- )} - -
-
+ {error.message} + + {error instanceof ClickHouseQueryError && ( + <> + + Sent Query: + + + + )} +
+
+
+ ) : graphResults.length === 0 ? ( +
+ No data found within time range. +
+ ) : ( + <> + setActiveClickPayload(undefined)} + /> + + + )} + ); } diff --git a/packages/app/src/components/ServiceDashboardDbQuerySidePanel.tsx b/packages/app/src/components/ServiceDashboardDbQuerySidePanel.tsx index 7490fcd06..08a97edd1 100644 --- a/packages/app/src/components/ServiceDashboardDbQuerySidePanel.tsx +++ b/packages/app/src/components/ServiceDashboardDbQuerySidePanel.tsx @@ -1,8 +1,8 @@ import { useCallback, useMemo } from 'react'; import { pick } from 'lodash'; import { parseAsString, useQueryState } from 'nuqs'; -import type { Filter } from '@hyperdx/common-utils/dist/types'; -import { Drawer, Grid, Group, Text } from '@mantine/core'; +import { DisplayType, type Filter } from '@hyperdx/common-utils/dist/types'; +import { Drawer, Grid, Text } from '@mantine/core'; import { IconServer } from '@tabler/icons-react'; import { INTEGER_NUMBER_FORMAT, MS_NUMBER_FORMAT } from '@/ChartUtils'; @@ -90,11 +90,9 @@ export default function ServiceDashboardDbQuerySidePanel({ - - Total Query Time - {source && expressions && ( - - Query Throughput - {source && expressions && ( - - 20 Top Most Time Consuming Operations - {source && ( - - Request Error Rate - {source && expressions && ( - - Request Throughput - {source && expressions && ( ({ useQueriedChartConfig: jest.fn(), })); +jest.mock('@/source', () => ({ + useSource: jest.fn().mockReturnValue({ data: null }), +})); + jest.mock('@/utils', () => ({ formatNumber: jest.fn(), omit: jest.fn((obj: Record, keys: string[]) => { diff --git a/packages/app/src/components/charts/ChartContainer.tsx b/packages/app/src/components/charts/ChartContainer.tsx new file mode 100644 index 000000000..3d6fd6054 --- /dev/null +++ b/packages/app/src/components/charts/ChartContainer.tsx @@ -0,0 +1,52 @@ +import { Group, Stack } from '@mantine/core'; + +interface ChartContainerProps { + title?: React.ReactNode; + toolbarItems?: React.ReactNode[]; + children: React.ReactNode; + disableReactiveContainer?: boolean; +} + +function ChartContainer({ + title, + toolbarItems, + children, + disableReactiveContainer, +}: ChartContainerProps) { + return ( + + {(!!title || !!toolbarItems?.length) && ( + + {title || } + {toolbarItems && {toolbarItems}} + + )} + {disableReactiveContainer ? ( + children + ) : ( +
+
+ {children} +
+
+ )} +
+ ); +} + +export default ChartContainer; diff --git a/packages/app/src/components/charts/DisplaySwitcher.tsx b/packages/app/src/components/charts/DisplaySwitcher.tsx new file mode 100644 index 000000000..a60e06872 --- /dev/null +++ b/packages/app/src/components/charts/DisplaySwitcher.tsx @@ -0,0 +1,42 @@ +import cx from 'classnames'; +import { ActionIcon, Group, Tooltip } from '@mantine/core'; + +interface DisplaySwitcherProps { + value: T | undefined; + onChange: (value: T) => void; + options: { + value: T; + label: string; + icon: React.ReactNode; + disabled?: boolean; + }[]; +} + +function DisplaySwitcher({ + value, + onChange, + options, +}: DisplaySwitcherProps) { + return ( + + {options.map(({ icon, label, value: optionValue, disabled }) => ( + + onChange(optionValue)} + > + {icon} + + + ))} + + ); +} + +export default DisplaySwitcher;