diff --git a/controlplane/src/core/repositories/analytics/MetricsRepository.ts b/controlplane/src/core/repositories/analytics/MetricsRepository.ts index 23f43ac7ae..e3b726a125 100644 --- a/controlplane/src/core/repositories/analytics/MetricsRepository.ts +++ b/controlplane/src/core/repositories/analytics/MetricsRepository.ts @@ -866,6 +866,7 @@ export class MetricsRepository { WHERE Timestamp >= startDate AND Timestamp <= endDate AND OrganizationID = {organizationId:String} AND FederatedGraphID = {federatedGraphId:String} + AND OperationHash IS NOT NULL AND OperationHash != '' ${whereSql ? `AND ${whereSql}` : ''} ${searchSql} ${introspectionFilter} @@ -901,6 +902,7 @@ export class MetricsRepository { WHERE Timestamp >= startDate AND Timestamp <= endDate AND OrganizationID = {organizationId:String} AND FederatedGraphID = {federatedGraphId:String} + AND OperationHash IS NOT NULL AND OperationHash != '' ${whereSql ? `AND ${whereSql}` : ''} ${searchSql} ${introspectionFilter} @@ -939,6 +941,7 @@ export class MetricsRepository { WHERE Timestamp >= startDate AND Timestamp <= endDate AND OrganizationID = {organizationId:String} AND FederatedGraphID = {federatedGraphId:String} + AND OperationHash IS NOT NULL AND OperationHash != '' ${whereSql ? `AND ${whereSql}` : ''} ${searchSql} ${introspectionFilter} @@ -1053,6 +1056,7 @@ export class MetricsRepository { WHERE Timestamp >= startDate AND Timestamp <= endDate AND OrganizationID = {organizationId:String} AND FederatedGraphID = {federatedGraphId:String} + AND OperationHash IS NOT NULL AND OperationHash != '' ${whereSql ? `AND ${whereSql}` : ''} ${searchSql} ${introspectionFilter} @@ -1092,6 +1096,7 @@ export class MetricsRepository { WHERE Timestamp >= startDate AND Timestamp <= endDate AND OrganizationID = {organizationId:String} AND FederatedGraphID = {federatedGraphId:String} + AND OperationHash IS NOT NULL AND OperationHash != '' ${whereSql ? `AND ${whereSql}` : ''} ${searchSql} ${introspectionFilter} diff --git a/studio/src/components/operations/client-usage-table.tsx b/studio/src/components/operations/client-usage-table.tsx index abafb050eb..cb66fe42e0 100644 --- a/studio/src/components/operations/client-usage-table.tsx +++ b/studio/src/components/operations/client-usage-table.tsx @@ -135,7 +135,7 @@ export const ClientUsageTable = ({ - {client.version} + {client.version || '-'} diff --git a/studio/src/components/operations/deprecated-fields-table.tsx b/studio/src/components/operations/deprecated-fields-table.tsx index 59f13b4cb2..a4b88cd212 100644 --- a/studio/src/components/operations/deprecated-fields-table.tsx +++ b/studio/src/components/operations/deprecated-fields-table.tsx @@ -9,6 +9,7 @@ import { Table, TableBody, TableCell, + TableFooter, TableHead, TableHeader, TableRow, @@ -16,7 +17,6 @@ import { } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { useRouter } from "next/router"; -import { Separator } from "../ui/separator"; interface DeprecatedField { fieldName: string; @@ -96,48 +96,70 @@ export const DeprecatedFieldsTable = ({ - Field Path - Actions + Field Path + Actions - - {isLoading ? ( - - - - - - ) : hasDeprecatedFields ? ( - deprecatedFields.map((field, index) => ( - - - - {field.path} - +
+
+ + + {isLoading ? ( + + + - - + + ) : hasDeprecatedFields ? ( + deprecatedFields.map((field, index) => ( + + + + {field.path} + + + + + + + )) + ) : ( + + + No deprecated fields found - )) - ) : ( - - - No deprecated fields found + )} + +
+
+ {!isLoading && hasDeprecatedFields && ( + + + + +
+ + Found {deprecatedFields.length}{" "} + {deprecatedFields.length === 1 + ? "deprecated field" + : "deprecated fields"} + +
- )} - -
+ + + )} ); diff --git a/studio/src/components/operations/operations-list.tsx b/studio/src/components/operations/operations-list.tsx index 52514ac68d..c4dec4abc5 100644 --- a/studio/src/components/operations/operations-list.tsx +++ b/studio/src/components/operations/operations-list.tsx @@ -146,13 +146,14 @@ const OperationItem = ({ case "errors": return operation.errorRate && operation.errorRate > 0 ? `${operation.errorRate.toFixed(2)}%` - : null; + : "-"; default: return null; } }; const selectedMetric = getSelectedMetric(); + const isLatencyAtCap = sortField === "latency" && operation.latency >= 10000; return (
{selectedMetric && ( -
- {selectedMetric} +
+ {selectedMetric} + {isLatencyAtCap && ( + + + * + + +

+ This operation may have taken longer than 10s. The displayed + 10s represents the maximum latency bucket (10s+). +

+
+
+ )}
)}
@@ -229,6 +243,38 @@ export const OperationsList = ({ className, sortField = "requests", }: OperationsListProps) => { + // Optimistic local state for immediate UI feedback + const [optimisticSelection, setOptimisticSelection] = useState<{ + hash: string; + name: string; + } | null>(null); + + // Use ref to track previous selectedOperation values to compare + const prevHashRef = useRef(undefined); + const prevNameRef = useRef(undefined); + + // Only update if values actually changed to prevent unnecessary re-renders + useEffect(() => { + const currentHash = selectedOperation?.hash; + const currentName = selectedOperation?.name; + + // Check if values actually changed + const hasChanged = + prevHashRef.current !== currentHash || + prevNameRef.current !== currentName; + + if (hasChanged) { + if (selectedOperation) { + setOptimisticSelection(selectedOperation); + } else { + // Clear optimistic selection when prop is cleared + setOptimisticSelection(null); + } + prevHashRef.current = currentHash; + prevNameRef.current = currentName; + } + }, [selectedOperation]); + if (isLoading) { return (
@@ -265,21 +311,30 @@ export const OperationsList = ({ // In that case, only match operations with empty name // If selectedOperationName has a value, match operations with that exact name const operationName = operation.name || ""; + + // Use optimistic selection for immediate feedback, fallback to prop + const currentSelection = optimisticSelection || selectedOperation; const isSelected = - selectedOperation?.hash === operation.hash && - (selectedOperation?.name === null || - selectedOperation?.name === undefined + currentSelection?.hash === operation.hash && + (currentSelection?.name === null || + currentSelection?.name === undefined ? true // No name filter set, match by hash only - : selectedOperation?.name === operationName); // Name filter exists, match exact name + : currentSelection?.name === operationName); // Name filter exists, match exact name return ( - onOperationSelect(operation.hash, operation.name || "") - } + onClick={() => { + // Update optimistic state immediately for instant UI feedback + setOptimisticSelection({ + hash: operation.hash, + name: operationName, + }); + // Then update URL (which will eventually sync back via prop) + onOperationSelect(operation.hash, operationName); + }} searchQuery={searchQuery} sortField={sortField} /> diff --git a/studio/src/components/operations/operations-search.tsx b/studio/src/components/operations/operations-search.tsx index b9661f95cb..006c90850f 100644 --- a/studio/src/components/operations/operations-search.tsx +++ b/studio/src/components/operations/operations-search.tsx @@ -7,6 +7,11 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { MagnifyingGlassIcon, XMarkIcon, @@ -222,21 +227,30 @@ export const OperationsSearch = ({ {/* Sort Controls */}
- + + + + + +

+ Sort {sortDirection === "desc" ? "ascending" : "descending"} +

+
+