diff --git a/frontend/src/pages/QueryInsightsPage.jsx b/frontend/src/pages/QueryInsightsPage.jsx index 3ebff95..4cc1333 100644 --- a/frontend/src/pages/QueryInsightsPage.jsx +++ b/frontend/src/pages/QueryInsightsPage.jsx @@ -2,6 +2,8 @@ import React, { useEffect, useState } from "react"; import { apiFetch } from "../api"; import { useAuth } from "../state"; +const PAGE_SIZE = 10; + function scoreQuery(row) { const mean = Number(row.mean_time || 0); const calls = Number(row.calls || 0); @@ -22,12 +24,50 @@ function compactSql(sql) { return sql.replace(/\s+/g, " ").trim(); } +function buildQueryTips(row) { + if (!row) return []; + const tips = []; + const sql = (row.query_text || "").toLowerCase(); + + if ((row.mean_time || 0) > 100) { + tips.push("High latency per call: inspect execution plan with EXPLAIN (ANALYZE, BUFFERS)."); + } + if ((row.total_time || 0) > 500) { + tips.push("High cumulative runtime: optimize this query first for fastest overall impact."); + } + if ((row.calls || 0) > 500) { + tips.push("Very frequent query: consider caching or reducing call frequency in application code."); + } + if ((row.rows || 0) > 100000) { + tips.push("Large row output: return fewer columns/rows or add pagination at query/API layer."); + } + if (/select\s+\*/.test(sql)) { + tips.push("Avoid SELECT *: request only required columns to reduce IO and transfer cost."); + } + if (/order\s+by/.test(sql) && !/limit\s+\d+/.test(sql)) { + tips.push("ORDER BY without LIMIT can be expensive: add LIMIT where possible."); + } + if (/where/.test(sql) && / from /.test(sql)) { + tips.push("Filter query detected: verify indexes exist on WHERE / JOIN columns."); + } + if (/like\s+'%/.test(sql)) { + tips.push("Leading wildcard LIKE can bypass indexes: consider trigram index (pg_trgm)."); + } + if (tips.length === 0) { + tips.push("No obvious anti-pattern detected. Validate with EXPLAIN and index usage statistics."); + } + + return tips.slice(0, 5); +} + 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 [search, setSearch] = useState(""); + const [page, setPage] = useState(1); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); @@ -52,17 +92,34 @@ export function QueryInsightsPage() { const data = await apiFetch(`/targets/${targetId}/top-queries`, {}, tokens, refresh); setRows(data); setSelectedQuery(data[0] || null); + setPage(1); } 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 dedupedByQueryId = [...rows].reduce((acc, row) => { + if (!row?.queryid) return acc; + if (!acc[row.queryid]) acc[row.queryid] = row; + return acc; + }, {}); + const uniqueRows = Object.values(dedupedByQueryId); + + const sorted = [...uniqueRows].sort((a, b) => (b.total_time || 0) - (a.total_time || 0)); + const byMean = [...uniqueRows].sort((a, b) => (b.mean_time || 0) - (a.mean_time || 0)); + const byCalls = [...uniqueRows].sort((a, b) => (b.calls || 0) - (a.calls || 0)); + const byRows = [...uniqueRows].sort((a, b) => (b.rows || 0) - (a.rows || 0)); + const byPriority = [...uniqueRows].sort((a, b) => scoreQuery(b) - scoreQuery(a)); + + const filtered = byPriority.filter((r) => { + if (!search.trim()) return true; + return compactSql(r.query_text).toLowerCase().includes(search.toLowerCase()); + }); + const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); + const safePage = Math.min(page, totalPages); + const paged = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE); + const selectedTips = buildQueryTips(selectedQuery); const categories = [ { key: "priority", title: "Optimization Priority", row: byPriority[0], subtitle: "Best first candidate to optimize" }, @@ -88,6 +145,17 @@ export function QueryInsightsPage() { ))} +
+ + { + setSearch(e.target.value); + setPage(1); + }} + placeholder="e.g. select * from users" + /> +
{loading ? ( @@ -147,10 +215,10 @@ export function QueryInsightsPage() { - {byPriority.map((r, i) => { + {paged.map((r, i) => { const state = classifyQuery(r); return ( - + {state.label} {r.calls} {Number(r.total_time || 0).toFixed(2)} @@ -166,6 +234,20 @@ export function QueryInsightsPage() { })} +
+ + Showing {paged.length} of {filtered.length} queries + +
+ + Page {safePage} / {totalPages} + +
+
@@ -181,6 +263,12 @@ export function QueryInsightsPage() {
{selectedQuery.query_text || "-- no query text available --"}
+
+

Optimization Suggestions

+ +

Tip: focus first on queries with high Total Time (overall impact) and high Mean Time (latency hotspots).

diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 717133c..a81a673 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -255,7 +255,8 @@ button { .query-insights-page .query-toolbar { display: grid; - grid-template-columns: minmax(240px, 420px); + grid-template-columns: minmax(220px, 320px) minmax(320px, 1fr); + gap: 12px; align-items: end; } @@ -346,6 +347,30 @@ button { font-family: "Space Grotesk", "Segoe UI", sans-serif; } +.pagination { + margin-top: 10px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; +} + +.pagination-info { + font-size: 12px; + color: #9eb8d6; +} + +.pagination-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.pagination-actions button { + min-height: 30px; + padding: 5px 10px; +} + .query-detail .sql-block { margin-top: 10px; background: #0a1c3a; @@ -365,6 +390,29 @@ button { line-height: 1.5; } +.query-tips { + margin-top: 10px; + border: 1px solid #2d5f92; + border-radius: 10px; + padding: 10px; + background: #0d2141; +} + +.query-tips h4 { + margin: 0 0 8px 0; +} + +.query-tips ul { + margin: 0; + padding-left: 16px; +} + +.query-tips li { + margin-bottom: 6px; + color: #d2e3fb; + font-size: 13px; +} + .query-hint { margin-top: 10px; color: #9eb8d6;