+
-
-
-
+ {mode === "live" && }
diff --git a/src/components/graphs/flamegraph.tsx b/src/components/graphs/flamegraph.tsx
index d959926..3125974 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
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ Tour
+
+
+
+
+
+
+
+
General Tour
+
Learn the basics of flamegraph
+
+
+
+ {mode === 'development' && (
+
+
+
+
+
Development Tour
+
Learn about advanced utilities and SetValues
+
+
+
+ )}
+ {mode === 'development' && tourOptions.map((option) => (
+
+
+
+
+
{option.name}
+
{option.description}
+
+
+
+ ))}
+
+
+
+
+
+
+
+ {explainFGLoading ? : }
+ Explain this profile
+
+
+
+ setDialogOpen(false)}
+ >
+
+ Close
+
+
+
+ {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 === "offline" && (
+
+
+
+
Utility Tools:
+
+ setUtilityBarOpen(!utilityBarOpen)}
+ className="px-3 py-1 text-xs bg-slate-700 hover:bg-slate-600 text-slate-300 rounded transition-colors"
+ >
+ {utilityBarOpen ? "Hide" : "Show"} Advanced 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:
+ setSelectedCount(parseInt(value))}>
+
+
+
+
+ 100
+ 500
+ 1000
+ 5000
+ 10000
+
+
+
+
+ {/* Fire SetValues Button */}
+
+
+ {setValuesLoading ? : }
+ {setValuesLoading ? "Abort SetValues" : "Fire SetValues"}
+
+ Seed data for analysis
+
+
+ {/* Stop Button */}
+
+
+
+ Stop Seeding
+
+ Cancel current operation
+
+
+
+ {/* Coming Soon - GET Request */}
+
+
+
+ Fire GET (Coming Soon)
+
+ Fetch data for analysis - Coming Soon
+
+
+
+ )}
+
+ )}
+
+
+
+
+ setSearchQuery((prev) => {
+ return {
+ ...prev,
+ searchQuery: e.target.value,
+ };
+ })
+ }
+ className="w-64 bg-slate-800 border-slate-700"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ Current Primary (P)
+
+
+ Secondary-1
+
+
+ Host
+
+
+
+
+
+
+
+
+
+ Table
+ Flamegraph
+ Both
+ Sandwich
+
+
+
+
+
+
+
+
+ Last 5 minutes
+ Last 30 minutes
+ Last 1 hour
+ Last 12 hours
+ Last 24 hours
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- Current Primary (P)
- {" "}
- {/* TODO: remove static coding */}
-
- Secondary-1
- {" "}
- {/* TODO: remove static coding */}
- Host
-
-
-
-
-
-
-
-
- Table
- Flamegraph
- Both
- Sandwich
-
-
-
-
-
-
-
-
- Last 5 minutes
- Last 30 minutes
- Last 1 hour
- Last 12 hours
- Last 24 hours
-
-
+
+ {loading ? (
+
+ ) : (
+
+ )}
-
- {loading ? (
-
- ) : (
-
- )}
-
- >
- )}
-
-
+ )}
+
+
+
);
};
diff --git a/src/components/graphs/lineGraph.tsx b/src/components/graphs/lineGraph.tsx
index 177077c..76ba75b 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)),
@@ -204,7 +203,7 @@ export const CpuLineGraphFunc: React.FC = ({ setDate }) => {
}
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 (
+
+
+
+
+
+ Retry
+
+
+
+ );
+ }
+
+ 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
+
+
+ Refresh
+
+
+
+
+ {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() {