From 5cdeb87e70f9482e7a5248681b831c3a0248253d Mon Sep 17 00:00:00 2001 From: sophiequynn Date: Fri, 5 Dec 2025 22:46:42 -0800 Subject: [PATCH 01/11] Add Dockerfile and update package.json for GraphQ-LLM integration - Add multi-stage Dockerfile for production builds - Update package name from memlens-client to reslens - Add --host flag to dev script for Docker networking - Add react-markdown and react-syntax-highlighter dependencies - Update @radix-ui/react-dialog version --- Dockerfile | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 16 ++++++--- 2 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b8c71e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,93 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Multi-stage build for ResLens Frontend +FROM node:20-alpine AS base + +# Install build dependencies +RUN apk add --no-cache libc6-compat python3 make g++ + +WORKDIR /app + +# Stage 1: Dependencies +FROM base AS deps +COPY package*.json ./ +RUN npm ci + +# Stage 2: Builder +FROM base AS builder +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build arguments for environment variables +ARG VITE_MIDDLEWARE_BASE_URL=http://reslens-middleware:3003/api/v1 +ENV VITE_MIDDLEWARE_BASE_URL=$VITE_MIDDLEWARE_BASE_URL + +ARG VITE_MIDDLEWARE_SECONDARY_BASE_URL=http://reslens-middleware:3003/api/v1 +ENV VITE_MIDDLEWARE_SECONDARY_BASE_URL=$VITE_MIDDLEWARE_SECONDARY_BASE_URL + +# Build the application +RUN npm run build + +# Stage 3: Production +FROM nginx:alpine AS runner + +# Copy built files to nginx +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration for SPA routing +RUN echo 'server { \ + listen 80; \ + server_name localhost; \ + root /usr/share/nginx/html; \ + index index.html; \ + \ + # Enable gzip compression \ + gzip on; \ + gzip_vary on; \ + gzip_min_length 1024; \ + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json; \ + \ + # SPA routing - serve index.html for all routes \ + location / { \ + try_files $uri $uri/ /index.html; \ + } \ + \ + # Cache static assets \ + location ~* \.(js|css|png|jpg|jpeg|jpg|gif|png|ico|svg|woff|woff2|ttf|eot)$ { \ + expires 1y; \ + add_header Cache-Control "public, immutable"; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1 + +CMD ["nginx", "-g", "daemon off;"] + diff --git a/package.json b/package.json index 6db9435..54f708c 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "memlens-client", + "name": "reslens", "private": true, "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" @@ -16,7 +16,7 @@ "@pyroscope/models": "^0.4.7", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-collapsible": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.7", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.2.1", @@ -53,18 +53,24 @@ "react-gauge-chart": "^0.5.1", "react-icons": "^5.3.0", "react-joyride": "^2.9.3", + "react-markdown": "^10.1.0", "react-router-dom": "^6.28.0", "react-slick": "^0.30.2", + "react-syntax-highlighter": "^15.6.1", "react-terminal-ui": "^1.3.0", "react-transition-group": "^4.4.5", "react-typed": "^2.0.12", "react-typewriter-effect": "^1.1.0", "react-wrap-balancer": "^1.1.1", "recharts": "^2.15.2", + "rehype-raw": "^7.0.0", + "rehype-starry-night": "^2.2.0", + "remark-gfm": "^4.0.1", "shadcn-ui": "^0.2.3", "slick-carousel": "^1.8.1", "tailwind-merge": "^2.5.4", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2" }, "devDependencies": { "@eslint/js": "^9.13.0", @@ -86,6 +92,6 @@ "tailwindcss": "^3.4.15", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", - "vite": "^5.4.10" + "vite": "^5.4.21" } } From c74f1fb32977abb70a519375b725746c79c279fd Mon Sep 17 00:00:00 2001 From: sophiequynn Date: Fri, 5 Dec 2025 22:47:38 -0800 Subject: [PATCH 02/11] Add additional routes and components from GraphQ-LLM modifications - Add CpuPage, MemoryPage, ExplorerPage routes - Add ResViewPage, FlamegraphPage, QueryStatsPage routes - Update App.tsx with ModeProvider and Loading components - Update dashboard and graph components --- src/App.css | 6 +- src/App.tsx | 36 +- src/components/dashboard/dashboard.tsx | 77 +- .../graphs/MemorySpecs/memoryMetricsGrid.tsx | 39 +- .../graphs/MemorySpecs/memoryTrackerPage.tsx | 15 +- .../MemorySpecs/storageEngineMetrics.tsx | 20 +- .../graphs/MemorySpecs/terminal.tsx | 15 +- src/components/graphs/TransactionChart.tsx | 7 +- src/components/graphs/cpuCard.tsx | 6 +- src/components/graphs/explorer.tsx | 13 +- src/components/graphs/flamegraph.tsx | 853 ++++++++++++++---- src/components/graphs/lineGraph.tsx | 25 +- src/components/graphs/queryStats.tsx | 344 +++++++ src/components/graphs/resView.tsx | 2 +- src/components/graphs/table.tsx | 7 +- 15 files changed, 1167 insertions(+), 298 deletions(-) create mode 100644 src/components/graphs/queryStats.tsx diff --git a/src/App.css b/src/App.css index e2487f2..d925196 100644 --- a/src/App.css +++ b/src/App.css @@ -63,4 +63,8 @@ iframe { pointer-events: auto; -} \ No newline at end of file +} + +.drawer-overlay { + background-color: rgba(0, 0, 0, 0.8) !important; /* 80% opaque */ +} diff --git a/src/App.tsx b/src/App.tsx index 30b24da..817de9a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,22 +22,38 @@ import { ThemeProvider } from "@/components/theme-provider"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { Dashboard } from "@/components/dashboard/dashboard"; import { HomePage } from "@/components/home-page/homePage"; -import PBFTPage from "@/components/home-page/PBFTFullView"; // Import the new PBFT page +import PBFTPage from "@/components/home-page/PBFTFullView"; import AuthorsPage from "./components/Authors/AuthorPage"; import { Toaster } from "./components/ui/toaster"; +import { CpuPage } from "@/pages/CpuPage"; +import { MemoryPage } from "@/pages/MemoryPage"; +import { ExplorerPage } from "@/pages/ExplorerPage"; +import { ResViewPage } from "@/pages/ResViewPage"; +import { FlamegraphPage } from "@/pages/FlamegraphPage"; +import { QueryStatsPage } from "@/pages/QueryStatsPage"; +import { ModeProvider } from "@/contexts/ModeContext"; +import { Loading } from "@/components/ui/loading"; function App() { return ( - - - - } /> - } /> - } /> {/* Add this */} - } /> - - + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); } diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index a600446..466799c 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -20,6 +20,7 @@ //@ts-nocheck import { useState, useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; import { motion } from "framer-motion"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Github } from "lucide-react"; @@ -27,14 +28,16 @@ import { DependencyGraph } from "../graphs/dependency"; import { startCase } from "@/lib/utils"; import { CpuPage } from "../graphs/cpuCard"; import { MemoryTrackerPage } from "../graphs/MemorySpecs/memoryTrackerPage"; -import { ToggleState, ModeType } from "../toggle"; -import { ModeContext } from "@/hooks/context"; -import { middlewareApi } from "@/lib/api"; +import { ToggleState } from "../toggle"; +import { useMode } from "@/contexts/ModeContext"; import Banner from "../ui/banner"; import { TourProvider, useTour } from "@/hooks/use-tour"; import { Tour } from "../ui/tour"; import { Explorer } from "../graphs/explorer"; import { ResView } from "../graphs/resView"; +import { QueryStats } from "../graphs/queryStats"; +import { DashboardNavigation } from "../navigation/DashboardNavigation"; +import { Loading } from "../ui/loading"; const tabVariants = { hidden: { opacity: 0, y: 10 }, @@ -90,44 +93,47 @@ export function Dashboard() { function DashboardComponent() { const { setSteps } = useTour(); - const [activeTab, setActiveTab] = useState("cpu"); - const [mode, setMode] = useState("offline"); - - async function getMode() { - try { - const response = await middlewareApi.get("/healthcheck"); - if (response?.status === 200) { - setMode("live"); - } else { - setMode("offline"); - } - } catch (error) { - setMode("offline"); - } - } + const [searchParams, setSearchParams] = useSearchParams(); + const tabFromUrl = searchParams.get("tab"); + const [activeTab, setActiveTab] = useState(tabFromUrl || "cpu"); + const { mode, isLoading, setMode } = useMode(); useEffect(() => { setSteps(steps); }, [setSteps]); + // Handle tab from URL parameter (when opened from Nexus) - priority over mode useEffect(() => { - getMode(); - }, []); - - useEffect(() => { - if (mode === "offline") { + if (tabFromUrl) { + console.log("Setting active tab from URL:", tabFromUrl); + setActiveTab(tabFromUrl); + } else if (mode === "development") { setActiveTab("cpu"); } - }, [mode]); + }, [tabFromUrl, mode]); + + // Log active tab changes for debugging + useEffect(() => { + console.log("Active tab changed to:", activeTab); + }, [activeTab]); + + // Update URL when tab changes + const handleTabChange = (value: string) => { + setActiveTab(value); + setSearchParams({ tab: value }, { replace: true }); + }; + + if (isLoading) { + return ; + } return ( - - {mode === "offline" && } + <>
@@ -202,10 +198,13 @@ function DashboardComponent() { + + +
-
+ ); } diff --git a/src/components/graphs/MemorySpecs/memoryMetricsGrid.tsx b/src/components/graphs/MemorySpecs/memoryMetricsGrid.tsx index ed199b5..7182efe 100644 --- a/src/components/graphs/MemorySpecs/memoryMetricsGrid.tsx +++ b/src/components/graphs/MemorySpecs/memoryMetricsGrid.tsx @@ -19,7 +19,7 @@ */ //@ts-nocheck -import { useState, useEffect, useContext } from "react"; +import { useState, useEffect } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { LineChart, @@ -37,9 +37,8 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { middlewareApi } from "@/lib/api"; import { ModeType } from "@/components/toggle"; -import { ModeContext } from "@/hooks/context"; +import { useMode } from "@/contexts/ModeContext"; import { useToast } from "@/hooks/use-toast"; import { DiskRWData, @@ -121,7 +120,7 @@ const MetricChart = ({ title, data, yAxisLabel, onRefresh, info }) => { export function MemoryMetricsGrid() { const { toast } = useToast(); - const mode = useContext(ModeContext); + const { mode, api, refreshTrigger } = useMode(); const [ioTimeData, setIoTimeData] = useState([]); const [diskRWData, setDiskRWData] = useState([]); const [diskIOPSData, setDiskIOPSData] = useState([]); @@ -130,16 +129,6 @@ export function MemoryMetricsGrid() { const [__, setErrorDiskIOPS] = useState(null); // Error state const fetchIoTimeData = async () => { - if (mode === "offline") { - setIoTimeData(TimeDuringIOData?.data); - toast({ - title: "Offline Mode", - description: "Using sample data in offline mode.", - variant: "default", - }); - return; - } - try { setLoadingDiskIOPS(true); // Set loading state setErrorDiskIOPS(null); // Clear errors @@ -148,7 +137,7 @@ export function MemoryMetricsGrid() { const from = new Date(until - 1 * 60 * 60 * 1000).getTime(); // 1 hour ago // Fetch data from the backend - const response = await middlewareApi.post( + const response = await api.post( "/nodeExporter/getTimeSpentDoingIO", { from: parseFloat((from / 1000).toFixed(3)), @@ -173,10 +162,6 @@ export function MemoryMetricsGrid() { }; const fetchDiskRWData = async () => { - if (mode === "offline") { - setDiskRWData(DiskRWData?.data?.writeMerged); - return; - } try { setLoadingDiskIOPS(true); // Indicate loading state setErrorDiskIOPS(null); // Clear previous errors @@ -186,7 +171,7 @@ export function MemoryMetricsGrid() { const step = 14; // Step size in seconds // Call the backend API - const response = await middlewareApi.post("/nodeExporter/getDiskRWData", { + const response = await api.post("/nodeExporter/getDiskRWData", { from: parseFloat((from / 1000).toFixed(3)), until: parseFloat((until / 1000).toFixed(3)), step, @@ -220,10 +205,6 @@ export function MemoryMetricsGrid() { }; const fetchDiskIOPSData = async () => { - if (mode === "offline") { - setDiskIOPSData(DiskIOPSData?.data); - return; - } try { setLoadingDiskIOPS(true); // Indicate loading state setErrorDiskIOPS(null); // Clear previous errors @@ -233,7 +214,7 @@ export function MemoryMetricsGrid() { const step = 14; // Step size in seconds // Call the backend API - const response = await middlewareApi.post("/nodeExporter/getDiskIOPS", { + const response = await api.post("/nodeExporter/getDiskIOPS", { from: parseFloat((from / 1000).toFixed(3)), until: parseFloat((until / 1000).toFixed(3)), step, @@ -259,10 +240,6 @@ export function MemoryMetricsGrid() { }; const fetchDiskWaitTimeData = async () => { - if (mode === "offline") { - setDiskWaitTimeData(DiskWaitTimeData?.data?.writeWaitTime); - return; - } try { setLoadingDiskIOPS(true); // Indicate loading state setErrorDiskIOPS(null); // Clear previous errors @@ -272,7 +249,7 @@ export function MemoryMetricsGrid() { const step = 14; // Step size in seconds // Call the backend API - const response = await middlewareApi.post( + const response = await api.post( "/nodeExporter/getDiskWaitTime", { from: parseFloat((from / 1000).toFixed(3)), @@ -315,7 +292,7 @@ export function MemoryMetricsGrid() { fetchDiskRWData(); fetchDiskIOPSData(); fetchDiskWaitTimeData(); - }, [mode]); + }, [refreshTrigger]); const metrics = [ { diff --git a/src/components/graphs/MemorySpecs/memoryTrackerPage.tsx b/src/components/graphs/MemorySpecs/memoryTrackerPage.tsx index 91bb399..b9b580b 100644 --- a/src/components/graphs/MemorySpecs/memoryTrackerPage.tsx +++ b/src/components/graphs/MemorySpecs/memoryTrackerPage.tsx @@ -18,15 +18,14 @@ * */ -import { StorageEngineMetrics } from "./storageEngineMetrics"; import { MemoryMetricsGrid } from "./memoryMetricsGrid"; import { Info, Map } from "lucide-react"; import { TerminalController } from "./terminal"; -import { useContext } from "react"; -import { ModeContext } from "@/hooks/context"; +import { useMode } from "@/contexts/ModeContext"; +import { StorageEngineMetrics } from "./storageEngineMetrics"; export function MemoryTrackerPage() { - const mode = useContext(ModeContext); + const { mode } = useMode(); return (
@@ -48,10 +47,10 @@ export function MemoryTrackerPage() {
- {!(mode === "offline") && } - {/* {import.meta.env.VITE_DISK_METRICS_FLAG === "true" && ( - - )} */} + {/* {!(mode === "development") && } */} + {(mode === "development") && ( + + )}
diff --git a/src/components/graphs/MemorySpecs/storageEngineMetrics.tsx b/src/components/graphs/MemorySpecs/storageEngineMetrics.tsx index 1bc4062..effbef3 100644 --- a/src/components/graphs/MemorySpecs/storageEngineMetrics.tsx +++ b/src/components/graphs/MemorySpecs/storageEngineMetrics.tsx @@ -45,8 +45,7 @@ import { } from "@/components/ui/table"; import { NotFound } from "@/components/ui/not-found"; import { useToast } from "@/hooks/use-toast"; -import { ModeType } from "@/components/toggle"; -import { ModeContext } from "@/hooks/context"; +import { useMode } from "@/contexts/ModeContext"; interface LevelDBStat { Level: string; @@ -89,6 +88,7 @@ interface LevelDBStatsTableProps { stats: LevelDBStat[]; } const CacheHitGauge = () => { + const { api, refreshTrigger } = useMode(); const [isCalculating, setIsCalculating] = useState(false); const [p99Value, setP99Value] = useState(0); const [animatedValue, setAnimatedValue] = useState(0); @@ -99,7 +99,7 @@ const CacheHitGauge = () => { setAnimatedValue(0); try { - const response = await middlewareApi.post("/transactions/calculateP99", { + const response = await api.post("/transactions/calculateP99", { samples: 100, }); const data = response?.data; @@ -169,28 +169,21 @@ const CacheHitGauge = () => { }; export function StorageEngineMetrics() { - const mode = useContext(ModeContext); const { toast } = useToast(); + const { mode, api, refreshTrigger } = useMode(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [refresh, setRefresh] = useState(false); const [data, setData] = useState(null); useEffect(() => { - if (mode === "offline") { - toast({ - title: "Offline Mode", - description: "Storage Engine Metrics Cannot be fetched in offline mode", - }); - return; - } fetchTransactionData(); - }, [refresh]); + }, [refresh, refreshTrigger]); async function fetchTransactionData() { try { setLoading(true); - const response = await middlewareApi.post( + const response = await api.post( "/statsExporter/getTransactionData" ); setData(response?.data); @@ -228,7 +221,6 @@ export function StorageEngineMetrics() { diff --git a/src/components/graphs/MemorySpecs/terminal.tsx b/src/components/graphs/MemorySpecs/terminal.tsx index 55c41a7..5ad70c5 100644 --- a/src/components/graphs/MemorySpecs/terminal.tsx +++ b/src/components/graphs/MemorySpecs/terminal.tsx @@ -21,14 +21,13 @@ //@ts-nocheck import { ModeType } from "@/components/toggle"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { ModeContext } from "@/hooks/context"; -import { middlewareApi } from "@/lib/api"; +import { useMode } from "@/contexts/ModeContext"; import { SquareTerminal } from "lucide-react"; -import { useContext, useState } from "react"; +import { useState } from "react"; import Terminal, { ColorMode, TerminalOutput } from "react-terminal-ui"; export const TerminalController = (props = {}) => { - const mode = useContext(ModeContext); + const { mode, api } = useMode(); const [terminalLineData, setTerminalLineData] = useState([ Welcome to ResilientDB Playground!, ]); @@ -38,8 +37,8 @@ export const TerminalController = (props = {}) => { let output: JSX.Element; if (args[0] === "resdb") { - if (mode === "offline") { - output =

Unable to run get and set method in offline mode

; + if (mode === "development") { + output =

Unable to run get and set method in development mode

; } else if ( args[1] === "set" && args[2] === "--key" && @@ -49,7 +48,7 @@ export const TerminalController = (props = {}) => { const key = args[3].replace(/"/g, ""); const value = args[5].replace(/"/g, ""); try { - const response = await middlewareApi.post("/transactions/set", { + const response = await api.post("/transactions/set", { id: key, value, }); @@ -76,7 +75,7 @@ export const TerminalController = (props = {}) => { } else if (args[1] === "get" && args[2] === "--key") { const key = args[3].replace(/"/g, ""); try { - const response = await middlewareApi.get(`/transactions/get/${key}`); + const response = await api.get(`/transactions/get/${key}`); if (response?.status !== 200) { output =

Error: Unable to get value for {key}

; return; diff --git a/src/components/graphs/TransactionChart.tsx b/src/components/graphs/TransactionChart.tsx index 0a92517..dc6f1b5 100644 --- a/src/components/graphs/TransactionChart.tsx +++ b/src/components/graphs/TransactionChart.tsx @@ -22,7 +22,7 @@ import { useState, useEffect, useRef } from "react" import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceArea, CartesianGrid } from "recharts"; import { Button } from "../ui/button" import { ChartContainer, ChartTooltipContent } from "../ui/LineGraphChart" -import { middlewareApi } from "@/lib/api" +import { useMode } from "@/contexts/ModeContext" import { RefreshCcw } from "lucide-react" import { Card, CardHeader, CardTitle, CardContent } from "../ui/card" @@ -42,6 +42,7 @@ const getAxisYDomain = (data, left, right, offset) => { } export default function TransactionZoomChart() { + const { api, refreshTrigger } = useMode(); const chartRef = useRef(null) const [chartData, setChartData] = useState([]) const [refAreaLeft, setRefAreaLeft] = useState(null) @@ -56,7 +57,7 @@ export default function TransactionZoomChart() { const fetchTransactionHistory = async () => { const { default: downsample } = await import("downsample-lttb") try { - const response = await middlewareApi.get("/explorer/getAllEncodedBlocks") + const response = await api.get("/explorer/getAllEncodedBlocks") const decoded = decodeDeltaEncoding(response?.data) const points = decoded.map((d) => [d.epoch / 1000, d.volume]) @@ -82,7 +83,7 @@ export default function TransactionZoomChart() { } fetchTransactionHistory() - }, []) + }, [refreshTrigger]) const zoom = () => { if (refAreaLeft === null || refAreaRight === null || refAreaLeft === refAreaRight) { diff --git a/src/components/graphs/cpuCard.tsx b/src/components/graphs/cpuCard.tsx index 916c217..4e56de6 100644 --- a/src/components/graphs/cpuCard.tsx +++ b/src/components/graphs/cpuCard.tsx @@ -22,11 +22,11 @@ import { useContext, useState } from "react"; import { Flamegraph } from "./flamegraph"; import { CpuLineGraphFunc } from "./lineGraph"; import { ModeType } from "../toggle"; -import { ModeContext } from "@/hooks/context"; +import { useMode } from "@/contexts/ModeContext"; import { TourProvider } from "@/hooks/use-tour"; export function CpuPage() { - const mode = useContext(ModeContext); + const { mode } = useMode(); const [date, setDate] = useState({ from: 0, until: 0, @@ -34,7 +34,7 @@ export function CpuPage() { return ( - {!(mode === "offline") && } + {(mode === "prod") && } ); diff --git a/src/components/graphs/explorer.tsx b/src/components/graphs/explorer.tsx index 3279378..b43a757 100644 --- a/src/components/graphs/explorer.tsx +++ b/src/components/graphs/explorer.tsx @@ -36,7 +36,7 @@ import { } from "../ui/card"; import { NotFound } from "../ui/not-found"; import { useEffect, useState } from "react"; -import { middlewareApi } from "@/lib/api"; +import { useMode } from "@/contexts/ModeContext"; import { Skeleton } from "../ui/skeleton"; import { formatSeconds } from "@/lib/utils"; import { Block, BlockchainTable } from "./table"; @@ -73,6 +73,7 @@ type ExplorerCardProps = { }; export function Explorer() { + const { api, refreshTrigger, mode } = useMode(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [configData, setData] = useState( @@ -87,7 +88,7 @@ export function Explorer() { async function fetchExplorerData() { try { setLoading(true); - const response = await middlewareApi.get("/explorer/getExplorerData"); + const response = await api.get("/explorer/getExplorerData"); setData(response?.data[0]); setLoading(false); } catch (error) { @@ -98,7 +99,7 @@ export function Explorer() { useEffect(() => { fetchExplorerData(); - }, []); + }, [refreshTrigger]); return (
@@ -120,11 +121,9 @@ export function Explorer() {
-
+
-
-
- + {mode === "prod" && }
diff --git a/src/components/graphs/flamegraph.tsx b/src/components/graphs/flamegraph.tsx index d959926..7172b97 100644 --- a/src/components/graphs/flamegraph.tsx +++ b/src/components/graphs/flamegraph.tsx @@ -1,22 +1,22 @@ /* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, -* software distributed under the License is distributed on an -* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -* KIND, either express or implied. See the License for the -* specific language governing permissions and limitations -* under the License. -* -*/ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ //@ts-nocheck import "@pyroscope/flamegraph/dist/index.css"; @@ -35,17 +35,24 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; +import { Dialog, DialogContent, DialogHeader } from "@/components/ui/dialog"; +import { TechnicalMarkdownRenderer } from "../ui/MarkdownRenderer"; import { FlamegraphRendererProps } from "@pyroscope/flamegraph/dist/packages/pyroscope-flamegraph/src/FlamegraphRenderer"; -import { middlewareApi } from "@/lib/api"; -import { Download, RefreshCcw, Flame, Info, Map } from "lucide-react"; +import { Download, RefreshCcw, Flame, Info, Map, Star, X, Play, BookOpen, Zap, Square } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Loader } from "../ui/loader"; import { NotFound } from "../ui/not-found"; -import { ModeContext } from "@/hooks/context"; +import { useMode } from "@/contexts/ModeContext"; import { ModeType } from "../toggle"; import { useToast } from "@/hooks/use-toast"; import { useTour } from "@/hooks/use-tour"; import { Tour } from "../ui/tour"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; const steps = [ { @@ -88,17 +95,68 @@ const steps = [ }, ]; +const tourOptions = [ + // { + // name: "Explore Advanced Tools", + // description: "Learn about the API testing and utility features", + // action: () => { + // console.log("Advanced tools tour"); + // } + // } +]; + +const developmentTourSteps = [ + { + content:

Welcome to Development Mode! Let's explore the advanced utilities.

, + placement: "center", + target: "body", + }, + { + content:

This utility bar contains advanced tools for testing and data generation

, + target: ".utility-bar", + }, + { + content:

Check the health status to ensure the seeding service is available

, + target: ".health-status", + }, + { + content:

Monitor the seeding status to see if operations are running

, + target: ".seeding-status", + }, + { + content:

Select the number of values you want to seed for testing

, + target: ".count-selector", + }, + { + content:

Click Fire SetValues to start seeding data for flamegraph analysis

, + target: ".fire-setvalues", + }, + { + content:

Watch the progress bar to track seeding completion

, + target: ".progress-bar", + }, + { + content:

Use the Stop Seeding button to cancel ongoing operations

, + target: ".stop-seeding", + }, + { + content:

You can abort operations at any time during the process

, + target: ".abort-button", + }, + { + content:

Once seeding completes, the flamegraph will refresh with new data

, + placement: "center", + target: "body", + }, +]; + interface FlamegraphCardProps { from: string | number; until: string | number; } export const Flamegraph = (props: FlamegraphCardProps) => { const { toast } = useToast(); - /** - * UI Client Data - */ - - const mode = useContext(ModeContext); + const { mode, api, refreshTrigger } = useMode(); const [clientName, setClientName] = useState("cpp_client_1"); const [profilingData, setProfilingData] = useState(ProfileData1); @@ -125,6 +183,18 @@ export const Flamegraph = (props: FlamegraphCardProps) => { syncEnabled: true, toggleSync: setSearchQueryToggle, }); + const [explainFGLoading, setExplainFGLoading] = useState(false); + const [explanationData, setExplanationData] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + const [utilityBarOpen, setUtilityBarOpen] = useState(false); + const [setValuesLoading, setSetValuesLoading] = useState(false); + const [seedingStatus, setSeedingStatus] = useState("stopped"); + const [healthStatus, setHealthStatus] = useState("unknown"); + const [selectedCount, setSelectedCount] = useState(1000); + const [abortController, setAbortController] = useState(null); + const [progress, setProgress] = useState(0); + const [resultCount, setResultCount] = useState(0); + const [pollingInterval, setPollingInterval] = useState(null); function handleFlamegraphTypeChange(value: string) { setFlamegraphDisplayType(value); @@ -134,6 +204,57 @@ export const Flamegraph = (props: FlamegraphCardProps) => { setFlamegraphInterval(value); } + async function explainFlamegraph() { + const from = props.from || flamegraphInterval; + const until = props.until || "now"; + + console.log(`Using ${mode} API endpoint for flamegraph explanation`); + + // Send the serialized data to /explainFlamegraph + try { + setExplainFGLoading(true); + const response = await api.post( + "/pyroscope/explainFlamegraph", + { + query: clientName, + from: from, + until: until, + } + ); + setExplainFGLoading(false); + console.log(response?.data); + setExplanationData(response?.data); + setDialogOpen(true); + + toast({ + title: "ExplainFlamegraph Response", + description: `Received response: Success`, + variant: "default", + }); + } catch (error) { + toast({ + title: "ExplainFlamegraph Response", + description: `Received response: Failure. Unable to recieve response ${error.message}`, + variant: "destructive", + }); + } finally { + setExplainFGLoading(false); + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function base64ToBlob( + base64Data: string, + contentType: string = "application/octet-stream" + ): Blob { + const byteCharacters = atob(base64Data); + const byteArrays = []; + for (let offset = 0; offset < byteCharacters.length; offset++) { + byteArrays.push(byteCharacters.charCodeAt(offset)); + } + return new Blob([new Uint8Array(byteArrays)], { type: contentType }); + } + function handleDownload() { const jsonData = JSON.stringify(profilingData, null, 2); const blob = new Blob([jsonData], { type: "application/json" }); @@ -150,23 +271,214 @@ export const Flamegraph = (props: FlamegraphCardProps) => { setRefresh((prev) => !prev); } + async function checkHealth() { + try { + const response = await fetch(`${import.meta.env.VITE_DEV_RESLENS_TOOLS_URL}/health`); + const data = await response.json(); + setHealthStatus(data.status); + return data.status === 'ok'; + } catch (error) { + setHealthStatus('error'); + return false; + } + } + + async function checkStatus() { + try { + const response = await fetch(`${import.meta.env.VITE_DEV_RESLENS_TOOLS_URL}/status`); + const data = await response.json(); + setSeedingStatus(data.status); + setResultCount(data.results_count || 0); + + // Calculate progress + if (data.status === 'running' && selectedCount > 0) { + const currentProgress = Math.min((data.results_count / selectedCount) * 100, 100); + setProgress(currentProgress); + } else if (data.status === 'stopped') { + setProgress(100); + } + + return data; + } catch (error) { + setSeedingStatus('error'); + return null; + } + } + + function startPolling() { + const interval = setInterval(async () => { + const statusData = await checkStatus(); + + if (statusData?.status === 'stopped') { + // Seeding is complete + clearInterval(interval); + setPollingInterval(null); + setSetValuesLoading(false); + setAbortController(null); + + toast({ + title: "Seeding Complete", + description: `Successfully seeded ${resultCount} values. Check the flamegraph for new data.`, + variant: "default", + }); + + // Refresh the flamegraph to show new data + refreshFlamegraph(); + } else if (statusData?.status === 'error') { + // Error occurred + clearInterval(interval); + setPollingInterval(null); + setSetValuesLoading(false); + setAbortController(null); + + toast({ + title: "Seeding Failed", + description: "An error occurred during seeding. Please try again.", + variant: "destructive", + }); + } + }, 1000); // Poll every second + + setPollingInterval(interval); + } + + function stopPolling() { + if (pollingInterval) { + clearInterval(pollingInterval); + setPollingInterval(null); + } + } + + async function fireSetValues() { + try { + // First check health + const isHealthy = await checkHealth(); + if (!isHealthy) { + toast({ + title: "Service Unavailable", + description: "The seeding service is not healthy. Please try again later.", + variant: "destructive", + }); + return; + } + + // Reset progress + setProgress(0); + setResultCount(0); + setSetValuesLoading(true); + const controller = new AbortController(); + setAbortController(controller); + + // Trigger the seeding job + const response = await fetch(`${import.meta.env.VITE_DEV_RESLENS_TOOLS_URL}/seed`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + count: selectedCount + }), + signal: controller.signal + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + toast({ + title: "SetValues Seeding Started", + description: `Started seeding ${selectedCount} values. Monitoring progress...`, + variant: "default", + }); + + // Start polling for status updates + startPolling(); + + } catch (error) { + if (error.name === 'AbortError') { + toast({ + title: "SetValues Seeding Aborted", + description: "The seeding operation was cancelled.", + variant: "default", + }); + } else { + toast({ + title: "SetValues Seeding Failed", + description: `${error.message || "Failed to start seeding"}. Please wait 30 seconds before trying again if needed.`, + variant: "destructive", + }); + } + setSetValuesLoading(false); + setAbortController(null); + } + } + + async function stopSeeding() { + try { + const response = await fetch(`${import.meta.env.VITE_DEV_RESLENS_TOOLS_URL}/stop`, { + method: 'POST' + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + toast({ + title: "Seeding Stopped", + description: "The seeding operation has been stopped.", + variant: "default", + }); + + // Stop polling and update status + stopPolling(); + setSetValuesLoading(false); + setAbortController(null); + await checkStatus(); + } catch (error) { + toast({ + title: "Stop Failed", + description: error.message || "Failed to stop seeding", + variant: "destructive", + }); + } + } + + function abortSetValues() { + if (abortController) { + abortController.abort(); + setSetValuesLoading(false); + setAbortController(null); + } + stopPolling(); + } + const { startTour, setSteps } = useTour(); useEffect(() => { setSteps(steps); }, []); + const startDevelopmentTour = () => { + setSteps(developmentTourSteps); + startTour(); + }; + + // Check initial status and health useEffect(() => { - if (mode === "offline") { - setProfilingData(ProfileData1); - toast({ - title: "Offline Mode", - description: "Using sample data in offline mode.", - variant: "default", - }); - return; - } + checkHealth(); + checkStatus(); + }, []); + + // Cleanup polling on unmount + useEffect(() => { + return () => { + stopPolling(); + }; + }, []); + useEffect(() => { + console.log(`Flamegraph: Mode changed to ${mode}, refreshTrigger: ${refreshTrigger}`); + const fetchData = async () => { try { setLoading(true); @@ -176,7 +488,10 @@ export const Flamegraph = (props: FlamegraphCardProps) => { from = props.from; until = props.until; } - const response = await middlewareApi.post("/pyroscope/getProfile", { + + console.log(`Using ${mode} API endpoint for flamegraph data`); + + const response = await api.post("/pyroscope/getProfile", { query: clientName, from: from, until: until, @@ -193,7 +508,7 @@ export const Flamegraph = (props: FlamegraphCardProps) => { setProfilingData(response?.data); toast({ title: "Data Updated", - description: `Flamegraph data updated for ${clientName}`, + description: `Flamegraph data updated for ${clientName} (${mode})`, variant: "default", }); } @@ -210,131 +525,355 @@ export const Flamegraph = (props: FlamegraphCardProps) => { } }; fetchData(); - }, [clientName, refresh, props.from, props.until, mode, flamegraphInterval]); + }, [clientName, refresh, props.from, props.until, flamegraphInterval, refreshTrigger]); return ( - - - -
-
- - Flamegraph -
-
- - - - - + + + + +
+
+ + Flamegraph + {mode === 'development' && ( +
+ + DEBUG MODE +
+ )} +
+
+ + + + + + + + + + + {mode === 'development' && ( + + + + )} + {mode === 'development' && tourOptions.map((option) => ( + + + + ))} + + + + + + + + + + + +
+ {explanationData ? ( + + ) : ( +
+ +
+ )} +
+
+
-
- - - {error ? ( - - ) : ( - <> -
-
- - setSearchQuery((prev) => { - return { - ...prev, - searchQuery: e.target.value, - }; - }) - } - className="w-64 bg-slate-800 border-slate-700" - /> + + + {error ? ( + + ) : ( +
+ {/* Utility Bar - Only show in development mode */} + {mode === "development" && ( +
+
+
+ Utility Tools: +
+ +
+
+
+ Mode: {mode} + + Client: {clientName} +
+
+ + {utilityBarOpen && ( +
+
+ {/* Status Indicators */} +
+
+ Health: + + {healthStatus} + +
+
+ Status: + + {seedingStatus} + +
+
+ + {/* Progress Bar */} + {setValuesLoading && seedingStatus === 'running' && ( +
+
+ Progress: + {resultCount} / {selectedCount} ({progress.toFixed(1)}%) +
+
+
+
+
+ )} + + {/* SetValues Controls */} +
+ {/* Count Selection */} +
+ Count: + +
+ + {/* Fire SetValues Button */} +
+ + Seed data for analysis +
+ + {/* Stop Button */} +
+ + Cancel current operation +
+
+ + {/* Coming Soon - GET Request */} +
+ + Fetch data for analysis - Coming Soon +
+
+
+ )} +
+ )} + +
+
+ + setSearchQuery((prev) => { + return { + ...prev, + searchQuery: e.target.value, + }; + }) + } + className="w-64 bg-slate-800 border-slate-700" + /> +
+
+ + + + + + + +
-
- - - - - - - +
+ {loading ? ( + + ) : ( + + )}
-
- {loading ? ( - - ) : ( - - )} -
- - )} - - + )} + + + ); }; diff --git a/src/components/graphs/lineGraph.tsx b/src/components/graphs/lineGraph.tsx index 177077c..edc3a93 100644 --- a/src/components/graphs/lineGraph.tsx +++ b/src/components/graphs/lineGraph.tsx @@ -37,7 +37,6 @@ import { ChartTooltip, ChartTooltipContent, } from "../ui/LineGraphChart"; -import { middlewareApi } from "@/lib/api"; import { Loader } from "../ui/loader"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; import { Cpu, Info, RefreshCcw } from "lucide-react"; @@ -50,7 +49,7 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; -import { ModeContext } from "@/hooks/context"; +import { useMode } from "@/contexts/ModeContext"; import { ModeType } from "../toggle"; interface DataPoint { @@ -114,26 +113,26 @@ interface CpuLineGraphProps { } export const CpuLineGraphFunc: React.FC = ({ setDate }) => { - const mode = useContext(ModeContext); + const { mode, api, refreshTrigger } = useMode(); const { toast } = useToast(); const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [refAreaLeft, setRefAreaLeft] = useState(null); - const [refAreaRight, setRefAreaRight] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [refresh, setRefresh] = useState(false); + const [hour, setHour] = useState(1); const [left, setLeft] = useState(0); const [right, setRight] = useState(0); - const [top, setTop] = useState(0); const [bottom, setBottom] = useState(0); - const [refresh, setRefresh] = useState(false); - const [error, setError] = useState(""); - const [hour, setHour] = useState(1); + const [top, setTop] = useState(0); + const [refAreaLeft, setRefAreaLeft] = useState(null); + const [refAreaRight, setRefAreaRight] = useState(null); const fetchData = async () => { try { const until = new Date().getTime(); const from = new Date(until - hour * 60 * 60 * 1000).getTime(); - const response = await middlewareApi.post("/nodeExporter/getCpuUsage", { + const response = await api.post("/nodeExporter/getCpuUsage", { query: "sum(rate(namedprocess_namegroup_cpu_seconds_total{groupname=~'.+'}[5m])) by (groupname) * 100", from: parseFloat((from / 1000).toFixed(3)), @@ -198,13 +197,13 @@ export const CpuLineGraphFunc: React.FC = ({ setDate }) => { } useEffect(() => { - if (mode === "offline") { + if (mode === "development") { setError("no data available"); return; } fetchData(); - }, [refresh, hour, mode]); + }, [refresh, hour, refreshTrigger]); const formatXAxis = (tickItem: number) => { const date = new Date(tickItem); diff --git a/src/components/graphs/queryStats.tsx b/src/components/graphs/queryStats.tsx new file mode 100644 index 0000000..b5e5f05 --- /dev/null +++ b/src/components/graphs/queryStats.tsx @@ -0,0 +1,344 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +* +*/ + +import { useState, useEffect } from "react"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { useMode } from "@/contexts/ModeContext"; +import { middlewareApi } from "@/lib/api"; +import { Loader2, RefreshCw, TrendingUp, Clock, Zap, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +interface QueryStat { + id: string; + query: string; + efficiency: { + score: number; + estimatedTime?: string; + resourceUsage?: string; + complexity?: string; + recommendations?: string[]; + }; + explanation?: { + explanation?: string; + complexity?: string; + }; + optimizations?: Array<{ + query?: string; + explanation?: string; + confidence?: number; + }>; + timestamp: string; + createdAt: string; +} + +interface AggregatedStats { + totalQueries: number; + avgEfficiency: number; + complexityDistribution: Record; + recentQueries: QueryStat[]; +} + +export function QueryStats() { + const { api, refreshTrigger } = useMode(); + const [queryStats, setQueryStats] = useState([]); + const [aggregatedStats, setAggregatedStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchQueryStats = async () => { + try { + setLoading(true); + setError(null); + + // Get base URL from environment + const baseUrl = import.meta.env.VITE_MIDDLEWARE_BASE_URL || "http://localhost:3003/api/v1"; + console.log("Fetching query stats from:", baseUrl); + + // Use fetch directly to have better error handling + const statsUrl = `${baseUrl}/queryStats?limit=20`; + console.log("Full URL:", statsUrl); + + const statsResponse = await fetch(statsUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!statsResponse.ok) { + throw new Error(`HTTP error! status: ${statsResponse.status}`); + } + + const statsData = await statsResponse.json(); + console.log("Query stats response:", statsData); + + if (statsData?.success) { + setQueryStats(statsData.data || []); + } + + // Fetch aggregated statistics + const aggregatedUrl = `${baseUrl}/queryStats/aggregated`; + const aggregatedResponse = await fetch(aggregatedUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!aggregatedResponse.ok) { + throw new Error(`HTTP error! status: ${aggregatedResponse.status}`); + } + + const aggregatedData = await aggregatedResponse.json(); + console.log("Aggregated stats response:", aggregatedData); + + if (aggregatedData?.success) { + setAggregatedStats(aggregatedData.data); + } + } catch (err: any) { + console.error("Error fetching query statistics:", err); + console.error("Error details:", { + message: err.message, + name: err.name, + stack: err.stack, + }); + + // More user-friendly error message + let errorMessage = "Failed to fetch query statistics."; + if (err.message?.includes("Failed to fetch") || err.message?.includes("NetworkError")) { + errorMessage = "Network error: Cannot connect to middleware. Please ensure ResLens Middleware is running on port 3003."; + } else if (err.message?.includes("HTTP error")) { + errorMessage = `Server error: ${err.message}`; + } else { + errorMessage = err.message || errorMessage; + } + + setError(errorMessage); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchQueryStats(); + }, [api, refreshTrigger]); + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( + + +
+ +

Error: {error}

+
+ +
+
+ ); + } + + return ( +
+ {/* Aggregated Statistics */} + {aggregatedStats && ( +
+ + + + + Total Queries + + + +

{aggregatedStats.totalQueries}

+

+ Queries analyzed +

+
+
+ + + + + + Average Efficiency + + + +

{aggregatedStats.avgEfficiency.toFixed(1)}

+

Out of 100

+ +
+
+ + + + + + Complexity Distribution + + + +
+ {Object.entries(aggregatedStats.complexityDistribution).map(([complexity, count]) => ( +
+ + {complexity} + + {count} +
+ ))} +
+
+
+
+ )} + + {/* Query Statistics List */} + + +
+ Query Statistics + +
+
+ + {queryStats.length === 0 ? ( +
+

No query statistics available yet.

+

+ Analyze queries in Nexus GraphQL Tutor to see statistics here. +

+
+ ) : ( +
+ {queryStats.map((stat) => ( + + +
+ {/* Query */} +
+

Query:

+ + {stat.query} + +
+ + {/* Efficiency Metrics */} +
+
+

Efficiency Score

+
+ = 80 ? "text-green-500" : + stat.efficiency.score >= 60 ? "text-yellow-500" : "text-red-500" + }`}> + {stat.efficiency.score} + + /100 +
+ +
+ +
+

Estimated Time

+

+ {stat.efficiency.estimatedTime || "N/A"} +

+
+ +
+

Complexity

+ + {stat.efficiency.complexity || "unknown"} + +
+ +
+

Resource Usage

+

+ {stat.efficiency.resourceUsage || "N/A"} +

+
+
+ + {/* Explanation */} + {stat.explanation?.explanation && ( +
+

Explanation:

+

+ {stat.explanation.explanation.substring(0, 200)} + {stat.explanation.explanation.length > 200 ? "..." : ""} +

+
+ )} + + {/* Optimizations */} + {stat.optimizations && stat.optimizations.length > 0 && ( +
+

Optimizations:

+
    + {stat.optimizations.slice(0, 3).map((opt, idx) => ( +
  • + {opt.explanation || "Optimization suggestion"} +
  • + ))} +
+
+ )} + + {/* Timestamp */} +
+ Analyzed: {new Date(stat.timestamp || stat.createdAt).toLocaleString()} +
+
+
+
+ ))} +
+ )} +
+
+
+ ); +} + diff --git a/src/components/graphs/resView.tsx b/src/components/graphs/resView.tsx index bfcf517..1965b9d 100644 --- a/src/components/graphs/resView.tsx +++ b/src/components/graphs/resView.tsx @@ -45,7 +45,7 @@ export function ResView() {