refactor: Add ChartContainer component with toolbar (#1560)

This commit is contained in:
Drew Davis 2026-01-07 15:29:35 -05:00 committed by GitHub
parent 725dbc2f3b
commit 158ccefa3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1055 additions and 895 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
refactor: Add ChartContainer component with toolbar

View file

@ -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 (
<Grid mt="md">
<Grid.Col span={6}>
<ChartBox style={{ minHeight: 400 }}>
<Text size="sm" mb="sm">
CPU Usage (Cores)
</Text>
<ChartBox style={{ height: 400 }}>
<DBTimeChart
title="CPU Usage (Cores)"
config={{
select: [
{
@ -80,17 +77,16 @@ function InfrastructureTab({
connection,
dateRange: searchedTimeRange,
timestampValueExpression: 'event_time',
displayType: DisplayType.Line,
}}
onTimeRangeSelect={onTimeRangeSelect}
/>
</ChartBox>
</Grid.Col>
<Grid.Col span={6}>
<ChartBox style={{ minHeight: 400 }}>
<Text size="sm" mb="sm">
Memory Usage
</Text>
<ChartBox style={{ height: 400 }}>
<DBTimeChart
title="Memory Usage"
config={{
select: [
{
@ -106,17 +102,16 @@ function InfrastructureTab({
connection,
dateRange: searchedTimeRange,
timestampValueExpression: 'event_time',
displayType: DisplayType.Line,
}}
onTimeRangeSelect={onTimeRangeSelect}
/>
</ChartBox>
</Grid.Col>
<Grid.Col span={6}>
<ChartBox style={{ minHeight: 400 }}>
<Text size="sm" mb="sm">
Disk
</Text>
<ChartBox style={{ height: 400 }}>
<DBTimeChart
title="Disk"
config={{
select: [
{
@ -140,17 +135,16 @@ function InfrastructureTab({
connection,
dateRange: searchedTimeRange,
timestampValueExpression: 'event_time',
displayType: DisplayType.Line,
}}
onTimeRangeSelect={onTimeRangeSelect}
/>
</ChartBox>
</Grid.Col>
<Grid.Col span={6}>
<ChartBox style={{ minHeight: 400 }}>
<Text size="sm" mb="sm">
S3 Requests
</Text>
<ChartBox style={{ height: 400 }}>
<DBTimeChart
title="S3 Requests"
config={{
select: [
{
@ -192,20 +186,25 @@ function InfrastructureTab({
where: '',
dateRange: searchedTimeRange,
timestampValueExpression: 'event_time',
displayType: DisplayType.Line,
}}
onTimeRangeSelect={onTimeRangeSelect}
/>
</ChartBox>
</Grid.Col>
<Grid.Col span={6}>
<ChartBox style={{ minHeight: 400 }}>
<Text size="sm" mb="xs">
Network
</Text>
<Text size="xs" mb="sm">
Network activity for the entire machine, not only Clickhouse.
</Text>
<ChartBox style={{ height: 400 }}>
<DBTimeChart
title={
<>
<Text size="sm" mb="xs">
Network
</Text>
<Text size="xs" mb="sm">
Network activity for the entire machine, not only Clickhouse.
</Text>
</>
}
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 (
<Grid mt="md">
<Grid.Col span={12}>
<ChartBox style={{ minHeight: 400 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">
Insert{' '}
{insertsBy === 'queries'
? 'Queries'
: insertsBy === 'rows'
? 'Rows'
: 'Bytes'}{' '}
Per Table
</Text>
<SegmentedControl
size="xs"
value={insertsBy ?? 'queries'}
onChange={value => {
// @ts-ignore
setInsertsBy(value);
}}
data={[
{ label: 'Queries', value: 'queries' },
{ label: 'Rows', value: 'rows' },
{ label: 'Bytes', value: 'bytes' },
]}
/>
</Group>
<ChartBox style={{ height: 400 }}>
<DBTimeChart
title={
<Text size="sm">
Insert{' '}
{insertsBy === 'queries'
? 'Queries'
: insertsBy === 'rows'
? 'Rows'
: 'Bytes'}{' '}
Per Table
</Text>
}
toolbarPrefix={[
<SegmentedControl
size="xs"
value={insertsBy ?? 'queries'}
onChange={value => {
// @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({
</ChartBox>
</Grid.Col>
<Grid.Col span={12}>
<ChartBox style={{ minHeight: 200, height: 200 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Max Active Parts per Partition</Text>
</Group>
<ChartBox style={{ height: 200 }}>
<DBTimeChart
title="Max Active Parts per Partition"
config={{
select: [
{
@ -354,15 +355,19 @@ function InsertsTab({
</Grid.Col>
<Grid.Col span={12}>
<ChartBox style={{ height: 400 }}>
<Text size="sm" mb="sm">
Active Parts Per Partition
</Text>
<Text size="xs" mb="md">
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.
</Text>
<DBTableChart
title={
<>
<Text size="sm" mb="sm">
Active Parts Per Partition
</Text>
<Text size="xs" mb="md">
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.
</Text>
</>
}
config={{
dateRange: searchedTimeRange,
select: [
@ -496,6 +501,29 @@ function ClickhousePage() {
];
}, [latencyFilter]);
const heatmapToolbarItems = useMemo(() => {
if (latencyFilter.latencyMin != null || latencyFilter.latencyMax != null) {
return [
<Button
key="heatmap-reset-latency-filter"
size="xs"
variant="subtle"
onClick={() => {
// Clears the min/max latency filters that are used to filter the query results
setLatencyFilter({
latencyMin: null,
latencyMax: null,
});
// Updates the URL state and triggers a new data fetch
onSearch(DEFAULT_INTERVAL);
}}
>
Reset
</Button>,
];
}
}, [latencyFilter, onSearch, setLatencyFilter]);
return (
<Box p="sm" data-testid="clickhouse-dashboard-page">
<OnboardingModal requireSource={false} />
@ -554,75 +582,11 @@ function ClickhousePage() {
</Tabs.List>
<Tabs.Panel value="selects">
<Grid mt="md">
{/* <Grid.Col span={12}>
<ChartBox style={{ minHeight: 300, height: 300 }}>
<Group justify="space-between" align="center" mb="md">
<Text size="sm" ms="xs">
Select P95 Query Latency
</Text>
<SegmentedControl
size="xs"
data={[
{ label: 'Latency', value: 'latency' },
{ label: 'Throughput', value: 'throughput' },
{ label: 'Errors', value: 'errors' },
]}
/>
</Group>
<DBTimeChart
config={{
select: [
{
aggFn: 'quantile',
level: 0.95,
valueExpression: 'query_duration_ms',
aggCondition: '',
alias: `"Query P95 (ms)"`,
},
],
displayType: DisplayType.Line,
dateRange: searchedTimeRange,
connection,
timestampValueExpression: 'event_time',
from,
granularity: 'auto',
where: `query_kind='Select' AND (
type='ExceptionWhileProcessing' OR type='QueryFinish'
)`,
filters,
}}
onTimeRangeSelect={(start, end) => {
onTimeRangeSelect(start, end);
}}
/>
</ChartBox>
</Grid.Col> */}
<Grid.Col span={12}>
<ChartBox style={{ height: 250 }}>
<Flex justify="space-between" align="center">
<Text size="sm" ms="xs">
Query Latency
</Text>
{latencyFilter.latencyMin != null ||
latencyFilter.latencyMax != null ? (
<Button
size="xs"
variant="subtle"
onClick={() => {
// Clears the min/max latency filters that are used to filter the query results
setLatencyFilter({
latencyMin: null,
latencyMax: null,
});
// Updates the URL state and triggers a new data fetch
onSearch(DEFAULT_INTERVAL);
}}
>
Reset
</Button>
) : null}
</Flex>
<DBHeatmapChart
title="Query Latency"
toolbarSuffix={heatmapToolbarItems}
config={{
displayType: DisplayType.Heatmap,
select: [
@ -656,11 +620,8 @@ function ClickhousePage() {
</Grid.Col>
<Grid.Col span={12}>
<ChartBox style={{ height: 400 }}>
<Text size="sm" mb="md">
Query Count by Table
</Text>
<DBTimeChart
title="Query Count by Table"
config={{
select: [
{
@ -688,6 +649,7 @@ function ClickhousePage() {
)`,
filters,
limit: { limit: 1000 }, // TODO: Cut off more intelligently
displayType: DisplayType.Line,
}}
onTimeRangeSelect={(start, end) => {
onTimeRangeSelect(start, end);
@ -697,10 +659,8 @@ function ClickhousePage() {
</Grid.Col>
<Grid.Col span={12}>
<ChartBox style={{ height: 400 }}>
<Text size="sm" mb="md">
Most Time Consuming Query Patterns
</Text>
<DBTableChart
title="Most Time Consuming Query Patterns"
config={{
select: [
{

View file

@ -75,19 +75,12 @@ import {
} from '@/dashboard';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import MVOptimizationIndicator from './components/MaterializedViews/MVOptimizationIndicator';
import OnboardingModal from './components/OnboardingModal';
import { Tags } from './components/Tags';
import useDashboardFilters from './hooks/useDashboardFilters';
import { useDashboardRefresh } from './hooks/useDashboardRefresh';
import { parseAsStringWithNewLines } from './utils/queryParsers';
import {
buildTableRowSearchUrl,
convertToNumberChartConfig,
convertToTableChartConfig,
convertToTimeChartConfig,
DEFAULT_CHART_CONFIG,
} from './ChartUtils';
import { buildTableRowSearchUrl, DEFAULT_CHART_CONFIG } from './ChartUtils';
import { IS_LOCAL_MODE } from './config';
import { useDashboard } from './dashboard';
import DashboardFilters from './DashboardFilters';
@ -173,25 +166,6 @@ const Tile = forwardRef(
ChartConfigWithDateRange | undefined
>(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 ? (
<Flex
gap="0px"
onMouseDown={e => e.stopPropagation()}
key="hover-toolbar"
>
{(chart.config.displayType === DisplayType.Line ||
chart.config.displayType === DisplayType.StackedBar) && (
<Indicator
size={alert?.state === AlertState.OK ? 6 : 8}
zIndex={1}
color={alertIndicatorColor}
processing={alert?.state === AlertState.ALERT}
label={!alert && <span className="fs-8">+</span>}
mr={4}
>
<Tooltip label={alertTooltip} withArrow>
<Button
data-testid={`tile-alerts-button-${chart.id}`}
variant="subtle"
color="gray"
size="xxs"
onClick={onEditClick}
>
<IconBell size={16} />
</Button>
</Tooltip>
</Indicator>
)}
<Button
data-testid={`tile-duplicate-button-${chart.id}`}
variant="subtle"
color="gray"
size="xxs"
onClick={onDuplicateClick}
title="Duplicate"
>
<IconCopy size={14} />
</Button>
<Button
data-testid={`tile-edit-button-${chart.id}`}
variant="subtle"
color="gray"
size="xxs"
onClick={onEditClick}
title="Edit"
>
<IconPencil size={14} />
</Button>
<Button
data-testid={`tile-delete-button-${chart.id}`}
variant="subtle"
color="gray"
size="xxs"
onClick={onDeleteClick}
title="Delete"
>
<IconTrash size={14} />
</Button>
</Flex>
) : (
<Box h={22} key="hover-empty-box" />
);
}, [
alert,
alertIndicatorColor,
alertTooltip,
chart.config.displayType,
chart.id,
hovered,
onDeleteClick,
onDuplicateClick,
onEditClick,
]);
const title = useMemo(
() => (
<Text size="sm" ms="xs">
{chart.config.name}
</Text>
),
[chart.config.name],
);
return (
<div
data-testid={`dashboard-tile-${chart.id}`}
className={`p-2 ${className} d-flex flex-column bg-muted rounded ${
className={`p-2 pt-0 ${className} d-flex flex-column bg-muted cursor-grab rounded ${
isHighlighted && 'dashboard-chart-highlighted'
}`}
id={`chart-${chart.id}`}
@ -268,84 +328,11 @@ const Tile = forwardRef(
onMouseUp={onMouseUp}
onTouchEnd={onTouchEnd}
>
<div className="d-flex justify-content-between align-items-center mb-2 cursor-grab">
<Text size="sm" ms="xs">
{chart.config.name}
</Text>
<Group>
{hovered ? (
<Flex gap="0px" onMouseDown={e => e.stopPropagation()}>
{(chart.config.displayType === DisplayType.Line ||
chart.config.displayType === DisplayType.StackedBar) && (
<Indicator
size={alert?.state === AlertState.OK ? 6 : 8}
zIndex={1}
color={alertIndicatorColor}
processing={alert?.state === AlertState.ALERT}
label={!alert && <span className="fs-8">+</span>}
mr={4}
>
<Tooltip label={alertTooltip} withArrow>
<Button
data-testid={`tile-alerts-button-${chart.id}`}
variant="subtle"
color="gray"
size="xxs"
onClick={onEditClick}
>
<IconBell size={16} />
</Button>
</Tooltip>
</Indicator>
)}
<Button
data-testid={`tile-duplicate-button-${chart.id}`}
variant="subtle"
color="gray"
size="xxs"
onClick={onDuplicateClick}
title="Duplicate"
>
<IconCopy size={14} />
</Button>
<Button
data-testid={`tile-edit-button-${chart.id}`}
variant="subtle"
color="gray"
size="xxs"
onClick={onEditClick}
title="Edit"
>
<IconPencil size={14} />
</Button>
<Button
data-testid={`tile-delete-button-${chart.id}`}
variant="subtle"
color="gray"
size="xxs"
onClick={onDeleteClick}
title="Delete"
>
<IconTrash size={14} />
</Button>
</Flex>
) : (
<Box h={22} />
)}
{source?.materializedViews?.length && queriedConfig && (
<Box onMouseDown={e => e.stopPropagation()}>
<MVOptimizationIndicator
config={configForMVOptimizationIndicator}
source={source}
variant="icon"
/>
</Box>
)}
</Group>
</div>
<Group justify="center" py={4}>
<Box bg={hovered ? 'gray' : undefined} w={100} h={2}></Box>
</Group>
<div
className="fs-7 text-muted flex-grow-1 overflow-hidden"
className="fs-7 text-muted flex-grow-1 overflow-hidden cursor-default"
onMouseDown={e => e.stopPropagation()}
>
<ErrorBoundary
@ -359,6 +346,8 @@ const Tile = forwardRef(
{(queriedConfig?.displayType === DisplayType.Line ||
queriedConfig?.displayType === DisplayType.StackedBar) && (
<DBTimeChart
title={title}
toolbarPrefix={[hoverToolbar]}
sourceId={chart.config.source}
showDisplaySwitcher={true}
config={queriedConfig}
@ -377,6 +366,8 @@ const Tile = forwardRef(
{queriedConfig?.displayType === DisplayType.Table && (
<Box p="xs" h="100%">
<DBTableChart
title={title}
toolbarPrefix={[hoverToolbar]}
config={queriedConfig}
getRowSearchLink={row =>
buildTableRowSearchUrl({
@ -390,10 +381,18 @@ const Tile = forwardRef(
</Box>
)}
{queriedConfig?.displayType === DisplayType.Number && (
<DBNumberChart config={queriedConfig} />
<DBNumberChart
title={title}
toolbarPrefix={[hoverToolbar]}
config={queriedConfig}
/>
)}
{queriedConfig?.displayType === DisplayType.Markdown && (
<HDXMarkdownChart config={queriedConfig} />
<HDXMarkdownChart
title={title}
toolbarItems={[hoverToolbar]}
config={queriedConfig}
/>
)}
{queriedConfig?.displayType === DisplayType.Search && (
<DBSqlRowTableWithSideBar

View file

@ -1826,6 +1826,7 @@ function DBSearchPage() {
config={histogramTimeChartConfig}
enabled={isReady}
showDisplaySwitcher={false}
showMVOptimizationIndicator={false}
queryKeyPrefix={QUERY_KEY_PREFIX}
onTimeRangeSelect={handleTimeRangeSelect}
/>
@ -1899,6 +1900,7 @@ function DBSearchPage() {
config={histogramTimeChartConfig}
enabled={isReady}
showDisplaySwitcher={false}
showMVOptimizationIndicator={false}
queryKeyPrefix={QUERY_KEY_PREFIX}
onTimeRangeSelect={handleTimeRangeSelect}
enableParallelQueries

View file

@ -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 (
<div className="hdx-markdown">
<ReactMarkdown>{markdown ?? ''}</ReactMarkdown>
</div>
<ChartContainer
title={title}
toolbarItems={toolbarItems}
disableReactiveContainer
>
<div className="hdx-markdown">
<ReactMarkdown>{markdown ?? ''}</ReactMarkdown>
</div>
</ChartContainer>
);
},
);

View file

@ -344,7 +344,7 @@ export const InfraPodsStatusTable = ({
return (
<Card p="md" data-testid="k8s-pods-table">
<Card.Section p="md" py="xs" withBorder>
<Card.Section p="md" py="xs">
<Group align="center" justify="space-between">
Pods
<SegmentedControl
@ -616,7 +616,7 @@ const NodesTable = ({
return (
<Card p="md" data-testid="k8s-nodes-table">
<Card.Section p="md" py="xs" withBorder>
<Card.Section p="md" py="xs">
Nodes
</Card.Section>
<Card.Section>
@ -815,7 +815,7 @@ const NamespacesTable = ({
return (
<Card p="md" data-testid="k8s-namespaces-table">
<Card.Section p="md" py="xs" withBorder>
<Card.Section p="md" py="xs">
Namespaces
</Card.Section>
<Card.Section>
@ -1297,12 +1297,10 @@ function KubernetesDashboardPage() {
<Grid>
<Grid.Col span={6}>
<Card p="md" data-testid="pod-cpu-usage-chart">
<Card.Section p="md" py="xs" withBorder>
CPU Usage
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
{metricSource && (
<DBTimeChart
title="CPU Usage"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -1336,12 +1334,10 @@ function KubernetesDashboardPage() {
</Grid.Col>
<Grid.Col span={6}>
<Card p="md" data-testid="pod-memory-usage-chart">
<Card.Section p="md" py="xs" withBorder>
Memory Usage
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
{metricSource && (
<DBTimeChart
title="Memory Usage"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -1384,7 +1380,7 @@ function KubernetesDashboardPage() {
</Grid.Col>
<Grid.Col span={12}>
<Card p="md" data-testid="k8s-warning-events-table">
<Card.Section p="md" py="xs" withBorder>
<Card.Section p="md" py="xs">
<Flex justify="space-between">
Latest Kubernetes Warning Events
{/*
@ -1464,12 +1460,10 @@ function KubernetesDashboardPage() {
<Grid>
<Grid.Col span={6}>
<Card p="md" data-testid="nodes-cpu-usage-chart">
<Card.Section p="md" py="xs" withBorder>
CPU Usage
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
{metricSource && (
<DBTimeChart
title="CPU Usage"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -1503,12 +1497,10 @@ function KubernetesDashboardPage() {
</Grid.Col>
<Grid.Col span={6}>
<Card p="md" data-testid="nodes-memory-usage-chart">
<Card.Section p="md" py="xs" withBorder>
Memory Usage
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
{metricSource && (
<DBTimeChart
title="Memory Usage"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -1555,12 +1547,10 @@ function KubernetesDashboardPage() {
<Grid>
<Grid.Col span={6}>
<Card p="md" data-testid="namespaces-cpu-usage-chart">
<Card.Section p="md" py="xs" withBorder>
CPU Usage
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
{metricSource && (
<DBTimeChart
title="CPU Usage"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -1594,12 +1584,10 @@ function KubernetesDashboardPage() {
</Grid.Col>
<Grid.Col span={6}>
<Card p="md" data-testid="namespaces-memory-usage-chart">
<Card.Section p="md" py="xs" withBorder>
Memory Usage
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
{metricSource && (
<DBTimeChart
title="Memory Usage"
config={convertV1ChartConfigToV2(
{
dateRange,

View file

@ -151,7 +151,7 @@ function NamespaceLogs({
return (
<Card p="md">
<Card.Section p="md" py="xs" withBorder>
<Card.Section p="md" py="xs">
<Flex justify="space-between" align="center">
Latest Namespace Logs & Spans
<Flex gap="xs" align="center">
@ -359,11 +359,9 @@ export default function NamespaceDetailsSidePanel({
/>
<Grid.Col span={6}>
<Card p="md" data-testid="namespace-details-cpu-usage-chart">
<Card.Section p="md" py="xs" withBorder>
CPU Usage by Pod
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
<DBTimeChart
title="CPU Usage by Pod"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -394,11 +392,9 @@ export default function NamespaceDetailsSidePanel({
</Grid.Col>
<Grid.Col span={6}>
<Card p="md" data-testid="namespace-details-memory-usage-chart">
<Card.Section p="md" py="xs" withBorder>
Memory Usage by Pod
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
<DBTimeChart
title="Memory Usage by Pod"
config={convertV1ChartConfigToV2(
{
dateRange,

View file

@ -1,20 +1,13 @@
import * as React from 'react';
import Link from 'next/link';
import { StringParam, useQueryParam, withDefault } from 'use-query-params';
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
import { TSource } from '@hyperdx/common-utils/dist/types';
import {
SearchConditionLanguage,
TSource,
} from '@hyperdx/common-utils/dist/types';
import {
Anchor,
Badge,
Box,
Card,
Drawer,
Flex,
Grid,
ScrollArea,
SegmentedControl,
Text,
} from '@mantine/core';
@ -167,7 +160,7 @@ function NodeLogs({
return (
<Card p="md">
<Card.Section p="md" py="xs" withBorder>
<Card.Section p="md" py="xs">
<Flex justify="space-between" align="center">
Latest Node Logs & Spans
<Flex gap="xs" align="center">
@ -375,11 +368,9 @@ export default function NodeDetailsSidePanel({
/>
<Grid.Col span={6}>
<Card p="md" data-testid="nodes-details-cpu-usage-chart">
<Card.Section p="md" py="xs" withBorder>
CPU Usage by Pod
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
<DBTimeChart
title="CPU Usage by Pod"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -410,11 +401,9 @@ export default function NodeDetailsSidePanel({
</Grid.Col>
<Grid.Col span={6} data-testid="nodes-details-memory-usage-chart">
<Card p="md">
<Card.Section p="md" py="xs" withBorder>
Memory Usage by Pod
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
<DBTimeChart
title="Memory Usage by Pod"
config={convertV1ChartConfigToV2(
{
dateRange,

View file

@ -183,7 +183,7 @@ function PodLogs({
return (
<Card p="md">
<Card.Section p="md" py="xs" withBorder>
<Card.Section p="md" py="xs">
<Flex justify="space-between" align="center">
Latest Pod Logs & Spans
<Flex gap="xs" align="center">
@ -368,11 +368,9 @@ export default function PodDetailsSidePanel({
/>
<Grid.Col span={6}>
<Card p="md" data-testid="pod-details-cpu-usage-chart">
<Card.Section p="md" py="xs" withBorder>
CPU Usage
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
<DBTimeChart
title="CPU Usage by Pod"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -404,11 +402,9 @@ export default function PodDetailsSidePanel({
</Grid.Col>
<Grid.Col span={6}>
<Card p="md" data-testid="pod-details-memory-usage-chart">
<Card.Section p="md" py="xs" withBorder>
Memory Usage
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
<DBTimeChart
title="Memory Usage"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -440,7 +436,7 @@ export default function PodDetailsSidePanel({
</Grid.Col>
<Grid.Col span={12}>
<Card p="md">
<Card.Section p="md" py="xs" withBorder>
<Card.Section p="md" py="xs">
Latest Pod Events
</Card.Section>
<Card.Section>

View file

@ -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 = (
<DisplaySwitcher
key="display-switcher"
value={latencyChartType}
onChange={setLatencyChartType}
options={[
{
value: 'line',
label: 'Display as Line Chart',
icon: <IconChartLine />,
},
{
value: 'histogram',
label: 'Display as Histogram',
icon: <IconChartHistogram />,
},
]}
/>
);
return (
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Request Latency</Text>
<div className="bg-muted px-2 py-1 rounded fs-8">
<Tooltip label="Display as Line Chart">
<ActionIcon
size="xs"
me={2}
className={cx({
'text-success': latencyChartType === 'line',
'text-muted-hover': latencyChartType !== 'line',
})}
onClick={() => setLatencyChartType('line')}
>
<IconChartLine />
</ActionIcon>
</Tooltip>
<Tooltip label="Display as Histogram">
<ActionIcon
size="xs"
className={cx({
'text-success': latencyChartType === 'histogram',
'text-muted-hover': latencyChartType !== 'histogram',
})}
onClick={() => setLatencyChartType('histogram')}
>
<IconChartHistogram />
</ActionIcon>
</Tooltip>
</div>
</Group>
{source &&
expressions &&
(latencyChartType === 'line' ? (
<DBTimeChart
title="Request Latency"
toolbarSuffix={[displaySwitcher]}
showDisplaySwitcher={false}
sourceId={source.id}
hiddenSeries={[
@ -312,6 +302,8 @@ export function EndpointLatencyChart({
/>
) : (
<DBHistogramChart
title="Request Latency"
toolbarSuffix={[displaySwitcher]}
config={{
source: source.id,
...pick(source, [
@ -543,20 +535,21 @@ function HttpTab({
<Grid mt="md" grow={false} w="100%" maw="100%" overflow="hidden">
<Grid.Col span={6}>
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Request Error Rate</Text>
<SegmentedControl
size="xs"
value={reqChartType}
onChange={setReqChartType}
data={[
{ label: 'Overall', value: 'overall' },
{ label: 'By Endpoint', value: 'endpoint' },
]}
/>
</Group>
{source && requestErrorRateConfig && (
<DBTimeChart
title="Request Error Rate"
toolbarSuffix={[
<SegmentedControl
key="request-error-rate-segmented-control"
size="xs"
value={reqChartType}
onChange={setReqChartType}
data={[
{ label: 'Overall', value: 'overall' },
{ label: 'By Endpoint', value: 'endpoint' },
]}
/>,
]}
sourceId={source.id}
hiddenSeries={['total_requests', 'error_requests']}
config={requestErrorRateConfig}
@ -569,11 +562,9 @@ function HttpTab({
</Grid.Col>
<Grid.Col span={6}>
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Request Throughput</Text>
</Group>
{source && expressions && (
<DBTimeChart
title="Request Throughput"
sourceId={source.id}
config={{
source: source.id,
@ -604,12 +595,9 @@ function HttpTab({
</Grid.Col>
<Grid.Col span={6}>
<ChartBox style={{ height: 350, overflow: 'auto' }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">20 Top Most Time Consuming Endpoints</Text>
</Group>
{source && expressions && (
<DBListBarChart
title="Top 20 Most Time Consuming Endpoints"
groupColumn="Endpoint"
valueColumn="Total (ms)"
getRowSearchLink={getRowSearchLink}
@ -715,29 +703,32 @@ function HttpTab({
</Grid.Col>
<Grid.Col span={12}>
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">
Top 20{' '}
{topEndpointsChartType === 'time'
? 'Most Time Consuming'
: 'Highest Error Rate'}
</Text>
<SegmentedControl
size="xs"
value={topEndpointsChartType}
onChange={(value: string) => {
if (value === 'time' || value === 'error') {
setTopEndpointsChartType(value);
}
}}
data={[
{ label: 'Sort by Time', value: 'time' },
{ label: 'Sort by Errors', value: 'error' },
]}
/>
</Group>
{source && expressions && (
<DBTableChart
title={
<Text size="sm">
Top 20{' '}
{topEndpointsChartType === 'time'
? 'Most Time Consuming'
: 'Highest Error Rate'}
</Text>
}
toolbarSuffix={[
<SegmentedControl
key="top-endpoints-chart-segmented-control"
size="xs"
value={topEndpointsChartType}
onChange={(value: string) => {
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 = (
<DisplaySwitcher
key="display-switcher"
value={chartType}
onChange={setChartType}
options={[
{
label: 'Show as List',
icon: <IconFilter size={14} />,
value: 'list',
},
{
label: 'Show as Table',
icon: <IconTable size={14} />,
value: 'table',
},
]}
/>
);
return (
<Grid mt="md" grow={false} w="100%" maw="100%" overflow="hidden">
<Grid.Col span={6}>
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Total Time Consumed per Query</Text>
</Group>
{source && totalTimePerQueryConfig && (
<DBTimeChart
title="Total Time Consumed per Query"
sourceId={source.id}
config={totalTimePerQueryConfig}
disableDrillDown
@ -1117,11 +1126,9 @@ function DatabaseTab({
</Grid.Col>
<Grid.Col span={6}>
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Throughput per Query</Text>
</Group>
{source && totalThroughputPerQueryConfig && (
<DBTimeChart
title="Throughput per Query"
sourceId={source.id}
config={totalThroughputPerQueryConfig}
disableQueryChunking
@ -1132,36 +1139,12 @@ function DatabaseTab({
</Grid.Col>
<Grid.Col span={12}>
<ChartBox style={{ height: 350, overflow: 'auto' }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Top 20 Most Time Consuming Queries</Text>
<Box>
<Button.Group>
<Button
variant="subtle"
color={chartType === 'list' ? 'green' : 'gray'}
size="xs"
title="List"
onClick={() => setChartType('list')}
>
<IconFilter size={14} />
</Button>
<Button
variant="subtle"
color={chartType === 'table' ? 'green' : 'gray'}
size="xs"
title="Table"
onClick={() => setChartType('table')}
>
<IconTable size={14} />
</Button>
</Button.Group>
</Box>
</Group>
{source &&
expressions &&
(chartType === 'list' ? (
<DBListBarChart
title="Top 20 Most Time Consuming Queries"
toolbarItems={[displaySwitcher]}
groupColumn="Statement"
valueColumn="Total"
hoverCardPosition="top-start"
@ -1246,6 +1229,8 @@ function DatabaseTab({
/>
) : (
<DBTableChart
title="Top 20 Most Time Consuming Queries"
toolbarSuffix={[displaySwitcher]}
getRowSearchLink={getRowSearchLink}
hiddenColumns={[
'duration_ns',
@ -1346,11 +1331,9 @@ function ErrorsTab({
<Grid mt="md" grow={false} w="100%" maw="100%" overflow="hidden">
<Grid.Col span={12}>
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Error Events per Service</Text>
</Group>
{source && expressions && (
<DBTimeChart
title="Error Events per Service"
sourceId={source.id}
config={{
source: source.id,

View file

@ -60,6 +60,7 @@ export const AlertPreviewChart = ({
<DBTimeChart
sourceId={source.id}
showDisplaySwitcher={false}
showMVOptimizationIndicator={false}
referenceLines={getAlertReferenceLines({ threshold, thresholdType })}
config={{
where: where || '',

View file

@ -1251,10 +1251,7 @@ export default function EditTimeChartForm({
</Paper>
) : undefined}
{queryReady && queriedConfig != null && activeTab === 'table' && (
<div
className="flex-grow-1 d-flex flex-column"
style={{ minHeight: 400 }}
>
<div className="flex-grow-1 d-flex flex-column" style={{ height: 400 }}>
<DBTableChart
config={queriedConfig}
getRowSearchLink={row =>
@ -1267,14 +1264,12 @@ export default function EditTimeChartForm({
}
onSortingChange={onTableSortingChange}
sort={tableSortState}
showMVOptimizationIndicator={false}
/>
</div>
)}
{queryReady && dbTimeChartConfig != null && activeTab === 'time' && (
<div
className="flex-grow-1 d-flex flex-column"
style={{ minHeight: 400 }}
>
<div className="flex-grow-1 d-flex flex-column" style={{ height: 400 }}>
<DBTimeChart
sourceId={sourceId}
config={dbTimeChartConfig}
@ -1286,15 +1281,16 @@ export default function EditTimeChartForm({
thresholdType: alert.thresholdType,
})
}
showMVOptimizationIndicator={false}
/>
</div>
)}
{queryReady && queriedConfig != null && activeTab === 'number' && (
<div
className="flex-grow-1 d-flex flex-column"
style={{ minHeight: 400 }}
>
<DBNumberChart config={queriedConfig} />
<div className="flex-grow-1 d-flex flex-column" style={{ height: 400 }}>
<DBNumberChart
config={queriedConfig}
showMVOptimizationIndicator={false}
/>
</div>
)}
{queryReady &&

View file

@ -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 (
<Paper shadow="xs" p="xl">
<Text size="sm" ta="center">
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 (
<ChartContainer
title={title}
toolbarItems={toolbarItemsMemo}
disableReactiveContainer
>
{isLoading || isMinMaxLoading ? (
<Text size="sm" ta="center" p="xl">
Loading...
</Text>
</Paper>
);
}
if (error || minMaxError) {
const _error: Error = error || minMaxError!;
return (
<Paper shadow="xs" p="xl" ta="center" h="100%">
<Text size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Button
className="mx-auto"
variant="subtle"
color="red"
onClick={() => errorModalControls.open()}
>
<Group gap="xxs">
<IconArrowsDiagonal size={16} />
See Error Details
</Group>
</Button>
<Modal
opened={errorModal}
onClose={() => errorModalControls.close()}
title="Error Details"
>
<Group align="start">
<Text size="sm" ta="center">
Error Message:
</Text>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{_error.message}
</Code>
{_error instanceof ClickHouseQueryError && (
<>
<Text my="sm" size="sm" ta="center">
Sent Query:
</Text>
<SQLPreview data={_error?.query} enableCopy />
</>
)}
</Group>
</Modal>
</Paper>
);
}
if (time.length < 2 || generatedTsBuckets?.length < 2) {
return (
<Paper shadow="xs" p="xl">
<Text size="sm" ta="center">
) : _error ? (
<Box p="xl" ta="center" h="100%">
<Text size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Button
className="mx-auto"
variant="subtle"
color="red"
onClick={() => errorModalControls.open()}
>
<Group gap="xxs">
<IconArrowsDiagonal size={16} />
See Error Details
</Group>
</Button>
<Modal
opened={errorModal}
onClose={() => errorModalControls.close()}
title="Error Details"
>
<Group align="start">
<Text size="sm" ta="center">
Error Message:
</Text>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{_error.message}
</Code>
{_error instanceof ClickHouseQueryError && (
<>
<Text my="sm" size="sm" ta="center">
Sent Query:
</Text>
<SQLPreview data={_error?.query} enableCopy />
</>
)}
</Group>
</Modal>
</Box>
) : time.length < 2 || generatedTsBuckets?.length < 2 ? (
<Text size="sm" ta="center" p="xl">
Not enough data points to render heatmap. Try expanding your search
criteria.
</Text>
</Paper>
);
}
return (
<Heatmap
key={JSON.stringify(config)}
data={[time, bucket, count]}
numberFormat={config.numberFormat}
onFilter={onFilter}
/>
) : (
<Heatmap
key={JSON.stringify(config)}
data={[time, bucket, count]}
numberFormat={config.numberFormat}
onFilter={onFilter}
/>
)}
</ChartContainer>
);
}

View file

@ -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 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError ? (
<div className="h-100 w-100 align-items-center justify-content-center text-muted">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Box mt="sm">
<Text my="sm" size="sm" ta="center">
Error Message:
</Text>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{error.message}
</Code>
{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(
<MVOptimizationIndicator
key="db-histogram-chart-mv-indicator"
config={config}
source={source}
variant="icon"
/>,
);
}
if (toolbarSuffix && toolbarSuffix.length > 0) {
allToolbarItems.push(...toolbarSuffix);
}
return allToolbarItems;
}, [
config,
toolbarPrefix,
toolbarSuffix,
source,
showMVOptimizationIndicator,
]);
return (
<ChartContainer title={title} toolbarItems={toolbarItemsMemo}>
{isLoading ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError ? (
<div className="h-100 w-100 align-items-center justify-content-center text-muted">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Box mt="sm">
<Text my="sm" size="sm" ta="center">
Sent Query:
Error Message:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Box>
</div>
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<div
// Hack, recharts will release real fix soon https://github.com/recharts/recharts/issues/172
style={{
position: 'relative',
width: '100%',
height: '100%',
}}
>
<div
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
top: 0,
}}
>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{error.message}
</Code>
{error instanceof ClickHouseQueryError && (
<>
<Text my="sm" size="sm" ta="center">
Sent Query:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Box>
</div>
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<HistogramChart graphResults={buckets} />
</div>
</div>
)}
</ChartContainer>
);
}

View file

@ -98,12 +98,10 @@ const InfraSubpanelGroup = ({
</Group>
</Group>
<SimpleGrid mt="md" cols={cols}>
<Card p="md" data-testid="cpu-usage-card">
<Card.Section p="md" py="xs" withBorder>
CPU Usage (%)
</Card.Section>
<Card.Section py={8} px={4} h={height}>
<Card data-testid="cpu-usage-card">
<Card.Section py={8} px={8} h={height}>
<DBTimeChart
title="CPU Usage (%)"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -130,12 +128,10 @@ const InfraSubpanelGroup = ({
/>
</Card.Section>
</Card>
<Card p="md" data-testid="memory-usage-card">
<Card.Section p="md" py="xs" withBorder>
Memory Used
</Card.Section>
<Card.Section py={8} px={4} h={height}>
<Card data-testid="memory-usage-card">
<Card.Section py={8} px={8} h={height}>
<DBTimeChart
title="Memory Used"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -162,12 +158,10 @@ const InfraSubpanelGroup = ({
/>
</Card.Section>
</Card>
<Card p="md" data-testid="disk-usage-card">
<Card.Section p="md" py="xs" withBorder>
Disk Available
</Card.Section>
<Card.Section py={8} px={4} h={height}>
<Card data-testid="disk-usage-card">
<Card.Section py={8} px={8} h={height}>
<DBTimeChart
title="Disk Available"
config={convertV1ChartConfigToV2(
{
dateRange,
@ -230,7 +224,7 @@ export default ({
)}
{source && (
<Card p="md" mt="xl">
<Card.Section p="md" py="xs" withBorder>
<Card.Section p="md" py="xs">
Pod Timeline
</Card.Section>
<Card.Section>

View file

@ -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 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError ? (
<div className="h-100 w-100 align-items-center justify-content-center text-muted">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Box mt="sm">
<Text my="sm" size="sm" ta="center">
Error Message:
</Text>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{error.message}
</Code>
{error instanceof ClickHouseQueryError && (
<>
const toolbarItemsMemo = useMemo(() => {
const allToolbarItems = [];
if (source && showMVOptimizationIndicator) {
allToolbarItems.push(
<MVOptimizationIndicator
key="db-list-bar-chart-mv-indicator"
config={config}
source={source}
variant="icon"
/>,
);
}
if (toolbarItems && toolbarItems.length > 0) {
allToolbarItems.push(...toolbarItems);
}
return allToolbarItems;
}, [config, source, toolbarItems, showMVOptimizationIndicator]);
return (
<ChartContainer title={title} toolbarItems={toolbarItemsMemo}>
{isLoading && !data ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError ? (
<div className="h-100 w-100 align-items-center justify-content-center text-muted">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Box mt="sm">
<Text my="sm" size="sm" ta="center">
Sent Query:
Error Message:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Box>
</div>
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<ListBar
data={data?.data ?? []}
columns={columns}
getRowSearchLink={getRowSearchLink}
hoverCardPosition={hoverCardPosition}
groupColumn={groupColumn}
valueColumn={valueColumn}
/>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{error.message}
</Code>
{error instanceof ClickHouseQueryError && (
<>
<Text my="sm" size="sm" ta="center">
Sent Query:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Box>
</div>
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<ListBar
data={data?.data ?? []}
columns={columns}
getRowSearchLink={getRowSearchLink}
hoverCardPosition={hoverCardPosition}
groupColumn={groupColumn}
valueColumn={valueColumn}
/>
)}
</ChartContainer>
);
}

View file

@ -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 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError ? (
<div className="h-100 w-100 align-items-center justify-content-center text-muted">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Box mt="sm">
<Text my="sm" size="sm" ta="center">
Error Message:
</Text>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{error.message}
</Code>
{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(
<MVOptimizationIndicator
key="db-number-chart-mv-indicator"
config={config}
source={source}
variant="icon"
/>,
);
}
if (toolbarSuffix && toolbarSuffix.length > 0) {
allToolbarItems.push(...toolbarSuffix);
}
return allToolbarItems;
}, [
config,
toolbarPrefix,
toolbarSuffix,
source,
showMVOptimizationIndicator,
]);
return (
<ChartContainer title={title} toolbarItems={toolbarItemsMemo}>
{isLoading && !data ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError ? (
<div className="h-100 w-100 align-items-center justify-content-center text-muted">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Box mt="sm">
<Text my="sm" size="sm" ta="center">
Sent Query:
Error Message:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Box>
</div>
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<Flex align="center" justify="center" h="100%" style={{ flexGrow: 1 }}>
<Text size="4rem">{number ?? 'N/A'}</Text>
</Flex>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{error.message}
</Code>
{error instanceof ClickHouseQueryError && (
<>
<Text my="sm" size="sm" ta="center">
Sent Query:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Box>
</div>
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<Flex align="center" justify="center" h="100%" style={{ flexGrow: 1 }}>
<Text size="4rem">{number ?? 'N/A'}</Text>
</Flex>
)}
</ChartContainer>
);
}

View file

@ -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<SortingState>([]);
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 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError && error ? (
<div className="h-100 w-100 align-items-center justify-content-center text-muted">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Box mt="sm">
<Text my="sm" size="sm" ta="center">
Error Message:
</Text>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{error.message}
</Code>
{error instanceof ClickHouseQueryError && (
<>
<Text my="sm" size="sm" ta="center">
Sent Query:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Box>
</div>
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<Table
data={data?.data ?? []}
columns={columns}
getRowSearchLink={getRowSearchLink}
sorting={effectiveSort}
onSortingChange={handleSortingChange}
tableBottom={
hasNextPage && (
<Text ref={fetchMoreRef} ta="center">
Loading...
const toolbarItemsMemo = useMemo(() => {
const allToolbarItems = [];
if (toolbarPrefix && toolbarPrefix.length > 0) {
allToolbarItems.push(...toolbarPrefix);
}
if (source && showMVOptimizationIndicator) {
allToolbarItems.push(
<MVOptimizationIndicator
key="db-table-chart-mv-indicator"
config={config}
source={source}
variant="icon"
/>,
);
}
if (toolbarSuffix && toolbarSuffix.length > 0) {
allToolbarItems.push(...toolbarSuffix);
}
return allToolbarItems;
}, [
config,
toolbarPrefix,
toolbarSuffix,
source,
showMVOptimizationIndicator,
]);
return (
<ChartContainer title={title} toolbarItems={toolbarItemsMemo}>
{isLoading && !data ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError && error ? (
<div className="h-100 w-100 align-items-center justify-content-center text-muted">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
)
}
/>
<Box mt="sm">
<Text my="sm" size="sm" ta="center">
Error Message:
</Text>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{error.message}
</Code>
{error instanceof ClickHouseQueryError && (
<>
<Text my="sm" size="sm" ta="center">
Sent Query:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Box>
</div>
) : data?.data.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<Table
data={data?.data ?? []}
columns={columns}
getRowSearchLink={getRowSearchLink}
sorting={effectiveSort}
onSortingChange={handleSortingChange}
tableBottom={
hasNextPage && (
<Text ref={fetchMoreRef} ta="center">
Loading...
</Text>
)
}
/>
)}
</ChartContainer>
);
}

View file

@ -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 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError ? (
<div className="h-100 w-100 d-flex g-1 flex-column align-items-center justify-content-center text-muted overflow-auto">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Button
className="mx-auto"
variant="subtle"
color="red"
onClick={() => errorExpansion.open()}
>
<Group gap="xxs">
<IconArrowsDiagonal size={16} />
See Error Details
</Group>
</Button>
<Modal
opened={isErrorExpanded}
onClose={() => errorExpansion.close()}
title="Error Details"
>
<Group align="start">
<Text size="sm" ta="center">
Error Message:
</Text>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
{error.message}
</Code>
{error instanceof ClickHouseQueryError && (
<>
<Text my="sm" size="sm" ta="center">
Sent Query:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Group>
</Modal>
</div>
) : graphResults.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<div
// Hack, recharts will release real fix soon https://github.com/recharts/recharts/issues/172
style={{
position: 'relative',
width: '100%',
height: '100%',
flexGrow: 1,
}}
>
<div
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
top: 0,
}}
>
<ActiveTimeTooltip
activeClickPayload={activeClickPayload}
buildSearchUrl={buildSearchUrl}
onDismiss={() => setActiveClickPayload(undefined)}
/>
{/* {totalGroups > groupKeys.length ? (
<div
className="bg-muted px-3 py-2 rounded fs-8"
style={{
zIndex: 5,
position: 'absolute',
top: 0,
left: 50,
visibility: 'visible',
}}
title={`Only the top ${groupKeys.length} groups are shown, ${
totalGroups - groupKeys.length
} groups are hidden. Try grouping by a different field.`}
>
<span className="text-muted-hover text-decoration-none fs-8">
<IconAlertTriangle size={14} style={{ display: 'inline' }} /> Only top{' '}
{groupKeys.length} groups shown
</span>
</div>
) : null*/}
{showDisplaySwitcher && (
<div
className="bg-muted px-2 py-1 rounded fs-8"
style={{
zIndex: 5,
position: 'absolute',
top: 0,
right: 0,
visibility: 'visible',
}}
>
<Tooltip label="Display as Line Chart">
<ActionIcon
size="xs"
me={2}
className={cx({
'text-success': displayType === 'line',
'text-muted-hover': displayType !== 'line',
})}
onClick={() => handleSetDisplayType(DisplayType.Line)}
>
<IconChartLine />
</ActionIcon>
</Tooltip>
const toolbarItemsMemo = useMemo(() => {
const allToolbarItems = [];
<Tooltip
label={
config.compareToPreviousPeriod
? 'Bar Chart Unavailable When Comparing to Previous Period'
: 'Display as Bar Chart'
}
>
<ActionIcon
size="xs"
className={cx({
'text-success': displayType === 'stacked_bar',
'text-muted-hover': displayType !== 'stacked_bar',
})}
disabled={config.compareToPreviousPeriod}
onClick={() => handleSetDisplayType(DisplayType.StackedBar)}
if (toolbarPrefix && toolbarPrefix.length > 0) {
allToolbarItems.push(...toolbarPrefix);
}
if (source && showMVOptimizationIndicator) {
allToolbarItems.push(
<MVOptimizationIndicator
key="db-time-chart-mv-indicator"
config={config}
source={source}
variant="icon"
/>,
);
}
if (showDisplaySwitcher) {
allToolbarItems.push(
<DisplaySwitcher
key="db-time-chart-display-switcher"
value={displayType}
onChange={handleSetDisplayType}
options={[
{
value: DisplayType.Line,
label: 'Display as Line Chart',
icon: <IconChartLine />,
},
{
value: DisplayType.StackedBar,
label: config.compareToPreviousPeriod
? 'Bar Chart Unavailable When Comparing to Previous Period'
: 'Display as Bar Chart',
icon: <IconChartBar />,
disabled: config.compareToPreviousPeriod,
},
]}
/>,
);
}
if (toolbarSuffix && toolbarSuffix.length > 0) {
allToolbarItems.push(...toolbarSuffix);
}
return allToolbarItems;
}, [
config,
displayType,
handleSetDisplayType,
showDisplaySwitcher,
source,
toolbarPrefix,
toolbarSuffix,
showMVOptimizationIndicator,
]);
return (
<ChartContainer title={title} toolbarItems={toolbarItemsMemo}>
{isLoading && !data ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
Loading Chart Data...
</div>
) : isError ? (
<div className="h-100 w-100 d-flex g-1 flex-column align-items-center justify-content-center text-muted overflow-auto">
<Text ta="center" size="sm" mt="sm">
Error loading chart, please check your query or try again later.
</Text>
<Button
className="mx-auto"
variant="subtle"
color="red"
onClick={() => errorExpansion.open()}
>
<Group gap="xxs">
<IconArrowsDiagonal size={16} />
See Error Details
</Group>
</Button>
<Modal
opened={isErrorExpanded}
onClose={() => errorExpansion.close()}
title="Error Details"
>
<Group align="start">
<Text size="sm" ta="center">
Error Message:
</Text>
<Code
block
style={{
whiteSpace: 'pre-wrap',
}}
>
<IconChartBar />
</ActionIcon>
</Tooltip>
</div>
)}
<MemoChart
dateRange={dateRange}
displayType={displayType}
graphResults={graphResults}
isClickActive={activeClickPayload}
lineData={lineData}
isLoading={isLoadingOrPlaceholder}
logReferenceTimestamp={logReferenceTimestamp}
numberFormat={config.numberFormat}
onTimeRangeSelect={onTimeRangeSelect}
referenceLines={referenceLines}
setIsClickActive={setActiveClickPayloadIfSourceAvailable}
showLegend={showLegend}
timestampKey={timestampColumn?.name}
previousPeriodOffsetSeconds={previousPeriodOffsetSeconds}
/>
</div>
</div>
{error.message}
</Code>
{error instanceof ClickHouseQueryError && (
<>
<Text my="sm" size="sm" ta="center">
Sent Query:
</Text>
<SQLPreview data={error?.query} />
</>
)}
</Group>
</Modal>
</div>
) : graphResults.length === 0 ? (
<div className="d-flex h-100 w-100 align-items-center justify-content-center text-muted">
No data found within time range.
</div>
) : (
<>
<ActiveTimeTooltip
activeClickPayload={activeClickPayload}
buildSearchUrl={buildSearchUrl}
onDismiss={() => setActiveClickPayload(undefined)}
/>
<MemoChart
dateRange={dateRange}
displayType={displayType}
graphResults={graphResults}
isClickActive={activeClickPayload}
lineData={lineData}
isLoading={isLoadingOrPlaceholder}
logReferenceTimestamp={logReferenceTimestamp}
numberFormat={config.numberFormat}
onTimeRangeSelect={onTimeRangeSelect}
referenceLines={referenceLines}
setIsClickActive={setActiveClickPayloadIfSourceAvailable}
showLegend={showLegend}
timestampKey={timestampColumn?.name}
previousPeriodOffsetSeconds={previousPeriodOffsetSeconds}
/>
</>
)}
</ChartContainer>
);
}

View file

@ -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({
<Grid grow={false} w="100%" maw="100%" overflow="hidden">
<Grid.Col span={6}>
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Total Query Time</Text>
</Group>
{source && expressions && (
<DBTimeChart
title="Total Query Time"
sourceId={sourceId}
hiddenSeries={['total_duration_ns']}
config={{
@ -119,6 +117,7 @@ export default function ServiceDashboardDbQuerySidePanel({
alias: 'Total Query Time',
},
],
displayType: DisplayType.Line,
numberFormat: MS_NUMBER_FORMAT,
filters: dbQueryFilters,
dateRange: searchedTimeRange,
@ -129,11 +128,9 @@ export default function ServiceDashboardDbQuerySidePanel({
</Grid.Col>
<Grid.Col span={6}>
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Query Throughput</Text>
</Group>
{source && expressions && (
<DBTimeChart
title="Query Throughput"
sourceId={sourceId}
config={{
source: source.id,
@ -157,6 +154,7 @@ export default function ServiceDashboardDbQuerySidePanel({
...INTEGER_NUMBER_FORMAT,
unit: 'requests',
},
displayType: DisplayType.Line,
filters: dbQueryFilters,
dateRange: searchedTimeRange,
}}

View file

@ -1,6 +1,5 @@
import { pick } from 'lodash';
import { TSource } from '@hyperdx/common-utils/dist/types';
import { Group, Text } from '@mantine/core';
import { MS_NUMBER_FORMAT } from '@/ChartUtils';
import { ChartBox } from '@/components/ChartBox';
@ -88,11 +87,9 @@ export default function ServiceDashboardEndpointPerformanceChart({
return (
<ChartBox style={{ height: 350, overflow: 'auto' }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">20 Top Most Time Consuming Operations</Text>
</Group>
{source && (
<DBListBarChart
title="20 Top Most Time Consuming Operations"
groupColumn="group"
valueColumn="Total Time Spent"
config={{

View file

@ -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 {
@ -97,11 +97,9 @@ export default function ServiceDashboardEndpointSidePanel({
<Grid grow={false} w="100%" maw="100%" overflow="hidden">
<Grid.Col span={6}>
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Request Error Rate</Text>
</Group>
{source && expressions && (
<DBTimeChart
title="Request Error Rate"
sourceId={source.id}
hiddenSeries={['total_count', 'error_count']}
config={{
@ -143,11 +141,9 @@ export default function ServiceDashboardEndpointSidePanel({
</Grid.Col>
<Grid.Col span={6}>
<ChartBox style={{ height: 350 }}>
<Group justify="space-between" align="center" mb="sm">
<Text size="sm">Request Throughput</Text>
</Group>
{source && expressions && (
<DBTimeChart
title="Request Throughput"
sourceId={source.id}
config={{
source: source.id,
@ -167,6 +163,7 @@ export default function ServiceDashboardEndpointSidePanel({
aggConditionLanguage: 'sql',
},
],
displayType: DisplayType.Line,
numberFormat: {
...INTEGER_NUMBER_FORMAT,
unit: 'requests',

View file

@ -1,25 +1,21 @@
import React from 'react';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { act, screen } from '@testing-library/react';
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
import { formatNumber } from '@/utils';
import { NumberFormat } from '../../types';
import DBNumberChart from '../DBNumberChart';
import { NumberFormatInput } from '../NumberFormat';
// Mock dependencies
jest.mock('@/hooks/useChartConfig', () => ({
useQueriedChartConfig: jest.fn(),
}));
jest.mock('@/source', () => ({
useSource: jest.fn().mockReturnValue({ data: null }),
}));
jest.mock('@/utils', () => ({
formatNumber: jest.fn(),
omit: jest.fn((obj: Record<string, unknown>, keys: string[]) => {

View file

@ -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 (
<Stack h="100%" w="100%" style={{ flexGrow: 1 }}>
{(!!title || !!toolbarItems?.length) && (
<Group justify="space-between" align="start">
{title || <span />}
{toolbarItems && <Group>{toolbarItems}</Group>}
</Group>
)}
{disableReactiveContainer ? (
children
) : (
<div
// Hack, recharts will release real fix soon https://github.com/recharts/recharts/issues/172
style={{
position: 'relative',
width: '100%',
height: '100%',
}}
>
<div
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
top: 0,
}}
>
{children}
</div>
</div>
)}
</Stack>
);
}
export default ChartContainer;

View file

@ -0,0 +1,42 @@
import cx from 'classnames';
import { ActionIcon, Group, Tooltip } from '@mantine/core';
interface DisplaySwitcherProps<T extends string> {
value: T | undefined;
onChange: (value: T) => void;
options: {
value: T;
label: string;
icon: React.ReactNode;
disabled?: boolean;
}[];
}
function DisplaySwitcher<T extends string>({
value,
onChange,
options,
}: DisplaySwitcherProps<T>) {
return (
<Group className="bg-muted px-2 py-2 rounded fs-8" align="center" gap={0}>
{options.map(({ icon, label, value: optionValue, disabled }) => (
<Tooltip label={label} key={optionValue}>
<ActionIcon
size="xs"
me={2}
className={cx({
'text-success': value === optionValue,
'text-muted-hover': value !== optionValue,
})}
disabled={disabled}
onClick={() => onChange(optionValue)}
>
{icon}
</ActionIcon>
</Tooltip>
))}
</Group>
);
}
export default DisplaySwitcher;