mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
refactor: Add ChartContainer component with toolbar (#1560)
This commit is contained in:
parent
725dbc2f3b
commit
158ccefa3f
25 changed files with 1055 additions and 895 deletions
5
.changeset/olive-turkeys-look.md
Normal file
5
.changeset/olive-turkeys-look.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
refactor: Add ChartContainer component with toolbar
|
||||
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export const AlertPreviewChart = ({
|
|||
<DBTimeChart
|
||||
sourceId={source.id}
|
||||
showDisplaySwitcher={false}
|
||||
showMVOptimizationIndicator={false}
|
||||
referenceLines={getAlertReferenceLines({ threshold, thresholdType })}
|
||||
config={{
|
||||
where: where || '',
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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[]) => {
|
||||
|
|
|
|||
52
packages/app/src/components/charts/ChartContainer.tsx
Normal file
52
packages/app/src/components/charts/ChartContainer.tsx
Normal 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;
|
||||
42
packages/app/src/components/charts/DisplaySwitcher.tsx
Normal file
42
packages/app/src/components/charts/DisplaySwitcher.tsx
Normal 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;
|
||||
Loading…
Reference in a new issue