Skip to content

Commit c9cb25c

Browse files
Merge pull request #2205 from redpanda-data/nf/observability-api
observability: new page based on dataplane observability API
2 parents 0d7d6e6 + 271f920 commit c9cb25c

18 files changed

Lines changed: 1301 additions & 29 deletions

File tree

frontend/bun.lock

Lines changed: 18 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
"react-markdown": "^10.1.0",
121121
"react-simple-code-editor": "^0.14.1",
122122
"react-syntax-highlighter": "^15.6.6",
123+
"recharts": "2.15.4",
123124
"remark-emoji": "^5.0.2",
124125
"remark-gfm": "^4.0.1",
125126
"remark-prism": "^1.3.6",

frontend/src/components/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export const FEATURE_FLAGS = {
2020
enableTranscriptsInConsole: false,
2121
enableApiKeyConfigurationAgent: false,
2222
shadowlinkCloudUi: false,
23+
enableDataplaneObservabilityServerless: false,
24+
enableDataplaneObservability: false,
2325
};
2426

2527
// Cloud-managed tag keys for service account integration
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/**
2+
* Copyright 2026 Redpanda Data, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License
5+
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
6+
*
7+
* As of the Change Date specified in that file, in accordance with
8+
* the Business Source License, use of this software will be governed
9+
* by the Apache License, Version 2.0
10+
*/
11+
12+
import { timestampFromMs } from '@bufbuild/protobuf/wkt';
13+
import type { FC } from 'react';
14+
import { useMemo } from 'react';
15+
import { useExecuteRangeQuery } from 'react-query/api/observability';
16+
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from 'recharts';
17+
18+
import { CHART_COLORS, transformTimeSeriesData } from './utils/chart-data';
19+
import { formatWithUnit } from '../../../utils/unit';
20+
import { Alert, AlertDescription } from '../../redpanda-ui/components/alert';
21+
import {
22+
ChartContainer,
23+
ChartLegend,
24+
ChartLegendContent,
25+
ChartTooltip,
26+
ChartTooltipContent,
27+
} from '../../redpanda-ui/components/chart';
28+
import { Skeleton } from '../../redpanda-ui/components/skeleton';
29+
import { Heading } from '../../redpanda-ui/components/typography';
30+
31+
type MetricChartProps = {
32+
queryName: string;
33+
timeRange: {
34+
start: Date;
35+
end: Date;
36+
};
37+
};
38+
39+
export const MetricChart: FC<MetricChartProps> = ({ queryName, timeRange }) => {
40+
const { data, isLoading, isError } = useExecuteRangeQuery({
41+
queryName,
42+
params: {
43+
start: timestampFromMs(timeRange.start.getTime()),
44+
end: timestampFromMs(timeRange.end.getTime()),
45+
filters: {},
46+
},
47+
});
48+
49+
// Transform the time series data into chart format
50+
const chartData = useMemo(() => transformTimeSeriesData(data?.results || []), [data]);
51+
52+
// Extract series names for creating lines
53+
const seriesNames = useMemo(() => {
54+
if (!data?.results) {
55+
return [];
56+
}
57+
return data.results
58+
.map((series) => series.name || 'value')
59+
.filter((name, index, self) => self.indexOf(name) === index);
60+
}, [data]);
61+
62+
// Chart configuration
63+
const chartConfig = useMemo(() => {
64+
const config: Record<string, { label: string; color: string }> = {};
65+
66+
for (let i = 0; i < seriesNames.length; i++) {
67+
config[seriesNames[i]] = {
68+
label: seriesNames[i],
69+
color: CHART_COLORS[i % CHART_COLORS.length],
70+
};
71+
}
72+
73+
return config;
74+
}, [seriesNames]);
75+
76+
if (isLoading) {
77+
return (
78+
<div className="rounded-md border border-gray-200 p-4">
79+
<Skeleton className="mt-2 h-[200px]" />
80+
</div>
81+
);
82+
}
83+
84+
if (isError || !data) {
85+
return (
86+
<div className="rounded-md border border-gray-200 p-4">
87+
<Alert className="mt-2" variant="warning">
88+
<AlertDescription>Failed to load data for this metric</AlertDescription>
89+
</Alert>
90+
</div>
91+
);
92+
}
93+
94+
if (chartData.length === 0) {
95+
return (
96+
<div className="rounded-md border border-gray-200 p-4">
97+
{data.metadata?.description ? (
98+
<Heading className="mb-4" level={4}>
99+
{data.metadata.description}
100+
</Heading>
101+
) : null}
102+
<Alert className="mt-2" variant="info">
103+
<AlertDescription>No data available for this time range</AlertDescription>
104+
</Alert>
105+
</div>
106+
);
107+
}
108+
109+
return (
110+
<div className="rounded-md border border-gray-200 p-4">
111+
{data.metadata?.description ? (
112+
<Heading className="mb-4" level={3}>
113+
{data.metadata.description}
114+
</Heading>
115+
) : null}
116+
117+
<ChartContainer className="mt-4 h-[250px] w-full" config={chartConfig}>
118+
<LineChart accessibilityLayer data={chartData}>
119+
<CartesianGrid strokeDasharray="3 3" vertical={false} />
120+
<XAxis
121+
axisLine={false}
122+
dataKey="timestamp"
123+
tickFormatter={(value) => {
124+
const date = new Date(value);
125+
return date.toLocaleTimeString('en-US', {
126+
hour: '2-digit',
127+
minute: '2-digit',
128+
timeZone: 'UTC',
129+
});
130+
}}
131+
tickLine={false}
132+
tickMargin={10}
133+
/>
134+
<YAxis
135+
axisLine={false}
136+
tickFormatter={(value) => formatWithUnit(value, data.metadata?.unit)}
137+
tickLine={false}
138+
width={80}
139+
/>
140+
<ChartTooltip
141+
content={
142+
<ChartTooltipContent
143+
className="min-w-[200px]"
144+
formatter={(value, name, item) => {
145+
const indicatorColor = item.payload.fill || item.color;
146+
const formattedValue = typeof value === 'number' ? formatWithUnit(value, data.metadata?.unit) : value;
147+
return (
148+
<div className="flex w-full items-center gap-3">
149+
<div className="h-2.5 w-2.5 shrink-0 rounded-[2px]" style={{ backgroundColor: indicatorColor }} />
150+
<span className="text-muted-foreground">{name}</span>
151+
<span className="ml-auto font-medium font-mono tabular-nums">{formattedValue}</span>
152+
</div>
153+
);
154+
}}
155+
hideLabel={false}
156+
labelFormatter={(_value, payload) => {
157+
const timestamp = payload?.[0]?.payload?.timestamp;
158+
if (!timestamp || typeof timestamp !== 'number') {
159+
return '';
160+
}
161+
const date = new Date(timestamp);
162+
if (!date.getTime()) {
163+
return '';
164+
}
165+
return date.toLocaleString('en-US', {
166+
month: 'short',
167+
day: 'numeric',
168+
hour: '2-digit',
169+
minute: '2-digit',
170+
timeZone: 'UTC',
171+
timeZoneName: 'short',
172+
});
173+
}}
174+
/>
175+
}
176+
/>
177+
{seriesNames.map((seriesName) => (
178+
<Line
179+
dataKey={seriesName}
180+
dot={false}
181+
key={seriesName}
182+
stroke={chartConfig[seriesName]?.color}
183+
strokeWidth={2}
184+
type="linear"
185+
/>
186+
))}
187+
<ChartLegend content={<ChartLegendContent />} />
188+
</LineChart>
189+
</ChartContainer>
190+
</div>
191+
);
192+
};

0 commit comments

Comments
 (0)