diff --git a/frontend/src/pages/QueryInsightsPage.jsx b/frontend/src/pages/QueryInsightsPage.jsx index 7bc8760..3ebff95 100644 --- a/frontend/src/pages/QueryInsightsPage.jsx +++ b/frontend/src/pages/QueryInsightsPage.jsx @@ -2,12 +2,34 @@ import React, { useEffect, useState } from "react"; import { apiFetch } from "../api"; import { useAuth } from "../state"; +function scoreQuery(row) { + const mean = Number(row.mean_time || 0); + const calls = Number(row.calls || 0); + const total = Number(row.total_time || 0); + const rows = Number(row.rows || 0); + return mean * 1.4 + total * 0.9 + calls * 0.2 + Math.min(rows / 50, 25); +} + +function classifyQuery(row) { + if ((row.mean_time || 0) > 100) return { label: "Very Slow", kind: "danger" }; + if ((row.total_time || 0) > 250) return { label: "Heavy", kind: "warn" }; + if ((row.calls || 0) > 500) return { label: "Frequent", kind: "info" }; + return { label: "Normal", kind: "ok" }; +} + +function compactSql(sql) { + if (!sql) return "-"; + return sql.replace(/\s+/g, " ").trim(); +} + export function QueryInsightsPage() { const { tokens, refresh } = useAuth(); const [targets, setTargets] = useState([]); const [targetId, setTargetId] = useState(""); const [rows, setRows] = useState([]); + const [selectedQuery, setSelectedQuery] = useState(null); const [error, setError] = useState(""); + const [loading, setLoading] = useState(true); useEffect(() => { (async () => { @@ -17,6 +39,8 @@ export function QueryInsightsPage() { if (t.length > 0) setTargetId(String(t[0].id)); } catch (e) { setError(String(e.message || e)); + } finally { + setLoading(false); } })(); }, []); @@ -27,53 +51,147 @@ export function QueryInsightsPage() { try { const data = await apiFetch(`/targets/${targetId}/top-queries`, {}, tokens, refresh); setRows(data); + setSelectedQuery(data[0] || null); } catch (e) { setError(String(e.message || e)); } })(); }, [targetId, tokens, refresh]); + const sorted = [...rows].sort((a, b) => (b.total_time || 0) - (a.total_time || 0)); + const byMean = [...rows].sort((a, b) => (b.mean_time || 0) - (a.mean_time || 0)); + const byCalls = [...rows].sort((a, b) => (b.calls || 0) - (a.calls || 0)); + const byRows = [...rows].sort((a, b) => (b.rows || 0) - (a.rows || 0)); + const byPriority = [...rows].sort((a, b) => scoreQuery(b) - scoreQuery(a)); + + const categories = [ + { key: "priority", title: "Optimization Priority", row: byPriority[0], subtitle: "Best first candidate to optimize" }, + { key: "total", title: "Longest Total Time", row: sorted[0], subtitle: "Biggest cumulative runtime impact" }, + { key: "mean", title: "Highest Mean Time", row: byMean[0], subtitle: "Slowest single-call latency" }, + { key: "calls", title: "Most Frequent", row: byCalls[0], subtitle: "Executed very often" }, + { key: "rows", title: "Most Rows Returned", row: byRows[0], subtitle: "Potentially heavy scans" }, + ]; + return ( -
Note: This section requires the pg_stat_statements extension on the monitored target.
| Time | -Calls | -Total ms | -Mean ms | -Rows | -Query | -
|---|---|---|---|---|---|
| {new Date(r.ts).toLocaleString()} | -{r.calls} | -{r.total_time.toFixed(2)} | -{r.mean_time.toFixed(2)} | -{r.rows} | -{r.query_text || "-"} | -
No data
+ )} +| Priority | +Calls | +Total ms | +Mean ms | +Rows | +Query Preview | +
|---|---|---|---|---|---|
| {state.label} | +{r.calls} | +{Number(r.total_time || 0).toFixed(2)} | +{Number(r.mean_time || 0).toFixed(2)} | +{r.rows} | ++ + | +
{selectedQuery.query_text || "-- no query text available --"}
+ + Tip: focus first on queries with high Total Time (overall impact) and high Mean Time (latency hotspots). +
+ > + ) : ( +No query selected.
+ )} +