Skip to content

Commit 644b3f8

Browse files
committed
feat: better performance page
1 parent 9d8c66f commit 644b3f8

File tree

7 files changed

+936
-422
lines changed

7 files changed

+936
-422
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
'use client';
2+
3+
import type { QueryPerformanceSummary } from '@databuddy/shared';
4+
import {
5+
Bar,
6+
BarChart,
7+
Cell,
8+
Pie,
9+
PieChart,
10+
ResponsiveContainer,
11+
Tooltip,
12+
XAxis,
13+
YAxis,
14+
} from 'recharts';
15+
16+
const CHART_COLORS = [
17+
'#3b82f6', // Blue
18+
'#10b981', // Emerald
19+
'#8b5cf6', // Violet
20+
'#f59e0b', // Amber
21+
'#ef4444', // Red
22+
];
23+
24+
const formatTime = (ms: number): string => {
25+
if (ms < 1000) {
26+
return `${ms.toFixed(1)}ms`;
27+
}
28+
return `${(ms / 1000).toFixed(2)}s`;
29+
};
30+
31+
const formatQuery = (query: string, maxLength = 30): string => {
32+
const cleaned = query
33+
.replace(/--.*$/gm, '')
34+
.replace(/\/\*[\s\S]*?\*\//g, '')
35+
.trim();
36+
if (cleaned.length <= maxLength) {
37+
return cleaned;
38+
}
39+
return `${cleaned.slice(0, maxLength)}...`;
40+
};
41+
42+
export const ResourceConsumptionChart = ({
43+
topQueriesByTime,
44+
onQueryClick,
45+
}: {
46+
topQueriesByTime: QueryPerformanceSummary[];
47+
onQueryClick?: (query: QueryPerformanceSummary) => void;
48+
}) => {
49+
const chartData = topQueriesByTime.slice(0, 5).map((query) => ({
50+
name: formatQuery(query.query, 25),
51+
value: query.percentage_of_total_time,
52+
fullQuery: query.query,
53+
originalQuery: query,
54+
}));
55+
56+
return (
57+
<div className="rounded-xl border bg-card shadow-sm">
58+
<div className="border-b px-6 py-4">
59+
<h3 className="font-semibold text-lg">Resource Consumption</h3>
60+
<p className="text-muted-foreground text-sm">
61+
Top queries by database time consumption
62+
</p>
63+
</div>
64+
<div className="p-6">
65+
<ResponsiveContainer height={300} width="100%">
66+
<PieChart>
67+
<Pie
68+
cx="50%"
69+
cy="50%"
70+
data={chartData}
71+
dataKey="value"
72+
innerRadius={60}
73+
onClick={(data) => {
74+
if (onQueryClick && data.originalQuery) {
75+
onQueryClick(data.originalQuery);
76+
}
77+
}}
78+
outerRadius={120}
79+
>
80+
{chartData.map((query, index) => (
81+
<Cell
82+
fill={CHART_COLORS[index % CHART_COLORS.length]}
83+
key={`cell-${index}-${query.name}`}
84+
style={{ cursor: onQueryClick ? 'pointer' : 'default' }}
85+
/>
86+
))}
87+
</Pie>
88+
<Tooltip
89+
content={({ active, payload }) => {
90+
if (active && payload && payload[0]) {
91+
const data = payload[0].payload;
92+
return (
93+
<div className="rounded-lg border bg-background p-3 shadow-lg">
94+
<p className="font-medium">{data.name}</p>
95+
<p className="text-muted-foreground text-sm">
96+
{data.value.toFixed(1)}% of total time
97+
</p>
98+
</div>
99+
);
100+
}
101+
return null;
102+
}}
103+
/>
104+
</PieChart>
105+
</ResponsiveContainer>
106+
</div>
107+
</div>
108+
);
109+
};
110+
111+
export const ResponseTimeChart = ({
112+
slowestQueries,
113+
onQueryClick,
114+
}: {
115+
slowestQueries: QueryPerformanceSummary[];
116+
onQueryClick?: (query: QueryPerformanceSummary) => void;
117+
}) => {
118+
const chartData = slowestQueries.slice(0, 8).map((query) => ({
119+
name: formatQuery(query.query, 20),
120+
avgTime: query.mean_exec_time,
121+
maxTime: query.max_exec_time,
122+
minTime: query.min_exec_time,
123+
fullQuery: query.query,
124+
originalQuery: query,
125+
}));
126+
127+
return (
128+
<div className="rounded-xl border bg-card shadow-sm">
129+
<div className="border-b px-6 py-4">
130+
<h3 className="font-semibold text-lg">Response Times</h3>
131+
<p className="text-muted-foreground text-sm">
132+
Average response time by query
133+
</p>
134+
</div>
135+
<div className="p-6">
136+
<ResponsiveContainer height={300} width="100%">
137+
<BarChart data={chartData}>
138+
<XAxis
139+
angle={-45}
140+
dataKey="name"
141+
height={80}
142+
textAnchor="end"
143+
tick={{ fontSize: 12 }}
144+
/>
145+
<YAxis tickFormatter={formatTime} />
146+
<Tooltip
147+
content={({ active, payload, label }) => {
148+
if (active && payload && payload[0]) {
149+
const data = payload[0].payload;
150+
return (
151+
<div className="rounded-lg border bg-background p-3 shadow-lg">
152+
<p className="font-medium">{label}</p>
153+
<p className="text-sm">
154+
<span className="text-muted-foreground">Avg:</span>{' '}
155+
{formatTime(data.avgTime)}
156+
</p>
157+
<p className="text-sm">
158+
<span className="text-muted-foreground">Min:</span>{' '}
159+
{formatTime(data.minTime)}
160+
</p>
161+
<p className="text-sm">
162+
<span className="text-muted-foreground">Max:</span>{' '}
163+
{formatTime(data.maxTime)}
164+
</p>
165+
</div>
166+
);
167+
}
168+
return null;
169+
}}
170+
/>
171+
<Bar
172+
dataKey="avgTime"
173+
fill="#ef4444"
174+
onClick={(data) => {
175+
if (onQueryClick && data.originalQuery) {
176+
onQueryClick(data.originalQuery);
177+
}
178+
}}
179+
style={{ cursor: onQueryClick ? 'pointer' : 'default' }}
180+
/>
181+
</BarChart>
182+
</ResponsiveContainer>
183+
</div>
184+
</div>
185+
);
186+
};

0 commit comments

Comments
 (0)