This is an automated email from the ASF dual-hosted git repository.

ankitsultana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new cf71cc02148 [timeseries] Added query stats panel to timeseries UI 
(#17423)
cf71cc02148 is described below

commit cf71cc0214866694b7d593d8e1120ac2a4f39180
Author: Shaurya Chaturvedi <[email protected]>
AuthorDate: Sun Dec 28 20:51:59 2025 -0800

    [timeseries] Added query stats panel to timeseries UI (#17423)
    
    * [timeseries] Added query stats panel to timeseries UI
    
    * Minimizing unnecessary changes
    
    ---------
    
    Co-authored-by: shauryachats <[email protected]>
---
 .../app/components/Query/TimeseriesQueryPage.tsx   | 106 +++++++------
 .../src/main/resources/app/pages/Query.tsx         |  30 +---
 .../main/resources/app/utils/PinotMethodUtils.ts   | 169 ++++++++++++---------
 3 files changed, 153 insertions(+), 152 deletions(-)

diff --git 
a/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
 
b/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
index c1a2b8d06bb..7e945070f3a 100644
--- 
a/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
+++ 
b/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
@@ -38,7 +38,7 @@ import { UnControlled as CodeMirror } from 
'react-codemirror2';
 import 'codemirror/lib/codemirror.css';
 import 'codemirror/theme/material.css';
 import 'codemirror/mode/javascript/javascript';
-import { getTimeSeriesQueryResult, getTimeSeriesLanguages } from 
'../../requests';
+import { getTimeSeriesLanguages } from '../../requests';
 import { useHistory, useLocation } from 'react-router';
 import TableToolbar from '../TableToolbar';
 import { Resizable } from 're-resizable';
@@ -46,21 +46,10 @@ import SimpleAccordion from '../SimpleAccordion';
 import TimeseriesChart from './TimeseriesChart';
 import MetricStatsTable from './MetricStatsTable';
 import { parseTimeseriesResponse, isBrokerFormat } from 
'../../utils/TimeseriesUtils';
-import { ChartSeries } from 'Models';
+import { ChartSeries, TableData } from 'Models';
 import { DEFAULT_SERIES_LIMIT } from '../../utils/ChartConstants';
-
-// Define proper types
-interface TimeseriesQueryResponse {
-  resultTable?: {
-    dataSchema: {
-      columnNames: string[];
-      columnDataTypes: string[];
-    };
-    rows: any[][];
-  };
-  exceptions?: any[];
-  error?: string;
-}
+import CustomizedTables from '../Table';
+import PinotMethodUtils from '../../utils/PinotMethodUtils';
 
 interface CodeMirrorEditor {
   getValue: () => string;
@@ -276,6 +265,10 @@ const TimeseriesQueryPage = () => {
   const [viewType, setViewType] = useState<'json' | 'chart'>('chart');
   const [selectedMetric, setSelectedMetric] = useState<string | null>(null);
   const [seriesLimitInput, setSeriesLimitInput] = 
useState<string>(DEFAULT_SERIES_LIMIT.toString());
+  const [queryStats, setQueryStats] = useState<TableData>({
+    columns: [],
+    records: [],
+  });
 
 
   // Fetch supported languages from controller configuration
@@ -364,38 +357,6 @@ const TimeseriesQueryPage = () => {
     handleQueryInterfaceKeyDownRef.current = handleQueryInterfaceKeyDown;
   }, [handleQueryInterfaceKeyDown]);
 
-  // Extract data processing logic
-  const processQueryResponse = useCallback((parsedData: 
TimeseriesQueryResponse) => {
-    setRawOutput(JSON.stringify(parsedData, null, 2));
-
-    // Check for errors
-    const errorMsg = parsedData.error ||
-      (parsedData.exceptions?.length > 0 ?
-        parsedData.exceptions.map((e: any) => e.message || 
e.toString()).join('; ') : '');
-    if (errorMsg) {
-      setError(errorMsg);
-      setChartSeries([]);
-      setTotalSeriesCount(0);
-      return;
-    }
-
-    // Parse broker response directly
-    if (isBrokerFormat(parsedData)) {
-      const series = parseTimeseriesResponse(parsedData);
-      setTotalSeriesCount(series.length);
-
-      // Create truncated series for visualization (limit to seriesLimitInput 
or default to DEFAULT_SERIES_LIMIT)
-      const limit = parseInt(seriesLimitInput, 10);
-      const effectiveLimit = !isNaN(limit) && limit > 0 ? limit : 
DEFAULT_SERIES_LIMIT;
-
-      const truncatedSeries = series.slice(0, effectiveLimit);
-      setChartSeries(truncatedSeries);
-    } else {
-      setChartSeries([]);
-      setTotalSeriesCount(0);
-    }
-  }, [seriesLimitInput]);
-
   const handleExecuteQuery = useCallback(async () => {
     if (!config.query.trim()) {
       setError('Please enter a query');
@@ -409,6 +370,7 @@ const TimeseriesQueryPage = () => {
     setSelectedMetric(null);
     setChartSeries([]);
     setTotalSeriesCount(0);
+    setQueryStats({ columns: [], records: [] });
 
     try {
       const requestPayload = {
@@ -421,14 +383,39 @@ const TimeseriesQueryPage = () => {
         queryOptions: `timeoutMs=${config.timeout}`
       };
 
-      const response = await getTimeSeriesQueryResult(requestPayload);
+      // Call the API and process with the shared broker response processor
+      const results = await 
PinotMethodUtils.getTimeseriesQueryResults(requestPayload);
+
+      // Set raw output
+      setRawOutput(JSON.stringify(results.data, null, 2));
+
+      // Set query stats (already extracted by the utility)
+      setQueryStats(results.queryStats || { columns: [], records: [] });
+
+      // Handle exceptions/errors
+      if (results.exceptions && results.exceptions.length > 0) {
+        const errorMsg = results.exceptions.map((e: any) => e.message || 
e.toString()).join('; ');
+        setError(errorMsg);
+        setChartSeries([]);
+        setTotalSeriesCount(0);
+        return;
+      }
+
+      // Parse timeseries data for visualization
+      if (isBrokerFormat(results.data)) {
+        const series = parseTimeseriesResponse(results.data);
+        setTotalSeriesCount(series.length);
 
-      // Handle response data - it might be stringified JSON or already an 
object
-      const parsedData = typeof response.data === 'string'
-        ? JSON.parse(response.data)
-        : response.data;
+        // Create truncated series for visualization
+        const limit = parseInt(seriesLimitInput, 10);
+        const effectiveLimit = !isNaN(limit) && limit > 0 ? limit : 
DEFAULT_SERIES_LIMIT;
 
-      processQueryResponse(parsedData);
+        const truncatedSeries = series.slice(0, effectiveLimit);
+        setChartSeries(truncatedSeries);
+      } else {
+        setChartSeries([]);
+        setTotalSeriesCount(0);
+      }
     } catch (error) {
       console.error('Error executing timeseries query:', error);
       const errorMessage = error.response?.data?.message || error.message || 
'Unknown error occurred';
@@ -436,7 +423,7 @@ const TimeseriesQueryPage = () => {
     } finally {
       setIsLoading(false);
     }
-  }, [config, updateURL, processQueryResponse]);
+  }, [config, updateURL, seriesLimitInput]);
 
   const copyToClipboard = () => {
     const aux = document.createElement('input');
@@ -562,6 +549,17 @@ const TimeseriesQueryPage = () => {
           </Grid>
         </Grid>
 
+        {queryStats.columns.length > 0 && (
+                <Grid item xs style={{ backgroundColor: 'white' }}>
+                  <CustomizedTables
+                    title="Query Response Stats"
+                    data={queryStats}
+                    showSearchBox={true}
+                    inAccordionFormat={true}
+                  />
+                </Grid>
+              )}
+
         {rawOutput && (
           <Grid item xs style={{ backgroundColor: 'white' }}>
                          <ViewToggle
diff --git a/pinot-controller/src/main/resources/app/pages/Query.tsx 
b/pinot-controller/src/main/resources/app/pages/Query.tsx
index 639807dd84a..692881e541f 100644
--- a/pinot-controller/src/main/resources/app/pages/Query.tsx
+++ b/pinot-controller/src/main/resources/app/pages/Query.tsx
@@ -42,7 +42,7 @@ import CustomizedTables from '../components/Table';
 import QuerySideBar from '../components/Query/QuerySideBar';
 import TableToolbar from '../components/TableToolbar';
 import SimpleAccordion from '../components/SimpleAccordion';
-import PinotMethodUtils from '../utils/PinotMethodUtils';
+import PinotMethodUtils, { QUERY_STATS_COLUMNS } from 
'../utils/PinotMethodUtils';
 import '../styles/styles.css';
 import {Resizable} from "re-resizable";
 import {useHistory, useLocation} from 'react-router';
@@ -146,32 +146,6 @@ const sqlFuntionsList = [
   'SUMMV', 'AVGMV', 'MINMAXRANGEMV', 'DISTINCTCOUNTMV', 
'DISTINCTCOUNTBITMAPMV', 'DISTINCTCOUNTHLLMV',
   'DISTINCTCOUNTRAWHLLMV', 'DISTINCT', 'ST_UNION'];
 
-const responseStatCols = [
-  'timeUsedMs',
-  'numDocsScanned',
-  'totalDocs',
-  'numServersQueried',
-  'numServersResponded',
-  'numSegmentsQueried',
-  'numSegmentsProcessed',
-  'numSegmentsMatched',
-  'numConsumingSegmentsQueried',
-  'numEntriesScannedInFilter',
-  'numEntriesScannedPostFilter',
-  'numGroupsLimitReached',
-  'numGroupsWarningLimitReached',
-  'partialResult',
-  'minConsumingFreshnessTimeMs',
-  'offlineThreadCpuTimeNs',
-  'realtimeThreadCpuTimeNs',
-  'offlineSystemActivitiesCpuTimeNs',
-  'realtimeSystemActivitiesCpuTimeNs',
-  'offlineResponseSerializationCpuTimeNs',
-  'realtimeResponseSerializationCpuTimeNs',
-  'offlineTotalCpuTimeNs',
-  'realtimeTotalCpuTimeNs'
-];
-
 // A custom hook that builds on useLocation to parse the query string
 function useQuery() {
   const { search } = useLocation();
@@ -389,7 +363,7 @@ const QueryPage = () => {
     const results = await PinotMethodUtils.getQueryResults(params);
     setResultError(results.exceptions || []);
     setResultData(results.result || { columns: [], records: [] });
-    setQueryStats(results.queryStats || { columns: responseStatCols, records: 
[] });
+    setQueryStats(results.queryStats || { columns: QUERY_STATS_COLUMNS, 
records: [] });
     setOutputResult(JSON.stringify(results.data, null, 2) || '');
     setStageStats(results?.data?.stageStats || {});
     setWarnings(extractWarnings(results));
diff --git a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts 
b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
index 4db98c7e728..584e92cb6ef 100644
--- a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
+++ b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
@@ -62,6 +62,7 @@ import {
   getQueryTables,
   getTableSchema,
   getQueryResult,
+  getTimeSeriesQueryResult,
   getTenantTable,
   getTableSize,
   getIdealState,
@@ -313,82 +314,106 @@ const getAsObject = (str: SQLResult) => {
   return str;
 };
 
+// Query stats column names (used for both SQL and Timeseries queries)
+const QUERY_STATS_COLUMNS = ['timeUsedMs',
+  'numDocsScanned',
+  'totalDocs',
+  'numServersQueried',
+  'numServersResponded',
+  'numSegmentsQueried',
+  'numSegmentsProcessed',
+  'numSegmentsMatched',
+  'numConsumingSegmentsQueried',
+  'numEntriesScannedInFilter',
+  'numEntriesScannedPostFilter',
+  'numGroupsLimitReached',
+  'numGroupsWarningLimitReached',
+  'partialResult',
+  'minConsumingFreshnessTimeMs',
+  'offlineThreadCpuTimeNs',
+  'realtimeThreadCpuTimeNs',
+  'offlineSystemActivitiesCpuTimeNs',
+  'realtimeSystemActivitiesCpuTimeNs',
+  'offlineResponseSerializationCpuTimeNs',
+  'realtimeResponseSerializationCpuTimeNs',
+  'offlineTotalCpuTimeNs',
+  'realtimeTotalCpuTimeNs'
+];
+
+// Extract query stats from broker response
+// This utility can be used for both SQL and Timeseries queries
+const extractQueryStatsFromResponse = (queryResponse) => {
+  const partialResult = queryResponse.partialResult ?? 
queryResponse.partialResponse;
+
+  return {
+    columns: QUERY_STATS_COLUMNS,
+    records: [[queryResponse.timeUsedMs, queryResponse.numDocsScanned, 
queryResponse.totalDocs, queryResponse.numServersQueried, 
queryResponse.numServersResponded,
+      queryResponse.numSegmentsQueried, queryResponse.numSegmentsProcessed, 
queryResponse.numSegmentsMatched, queryResponse.numConsumingSegmentsQueried,
+      queryResponse.numEntriesScannedInFilter, 
queryResponse.numEntriesScannedPostFilter, queryResponse.numGroupsLimitReached, 
queryResponse.numGroupsWarningLimitReached,
+      partialResult ?? '-', queryResponse.minConsumingFreshnessTimeMs,
+      queryResponse.offlineThreadCpuTimeNs, 
queryResponse.realtimeThreadCpuTimeNs,
+      queryResponse.offlineSystemActivitiesCpuTimeNs, 
queryResponse.realtimeSystemActivitiesCpuTimeNs,
+      queryResponse.offlineResponseSerializationCpuTimeNs, 
queryResponse.realtimeResponseSerializationCpuTimeNs,
+      queryResponse.offlineTotalCpuTimeNs, 
queryResponse.realtimeTotalCpuTimeNs]]
+  };
+};
+
+// Process broker response (used for both SQL and Timeseries queries)
+// Both APIs return BrokerResponseNativeV2 structure
+const processBrokerResponse = (queryResponse) => {
+  let exceptions: SqlException[] = [];
+  let dataArray = [];
+  let columnList = [];
+
+  // if sql api throws error, handle here
+  if(typeof queryResponse === 'string'){
+    exceptions.push({errorCode: null, message: queryResponse});
+  }
+  // if sql api returns a structured error with a `code`, handle here
+  if (queryResponse && queryResponse.code) {
+    if (queryResponse.error) {
+      exceptions.push({errorCode: null, message: "Query failed with error 
code: " + queryResponse.code + " and error: " + queryResponse.error});
+    } else {
+      exceptions.push({errorCode: null, message: "Query failed with error 
code: " + queryResponse.code + " but no logs. Please see controller logs for 
error."});
+    }
+  }
+  if (queryResponse && queryResponse.exceptions && 
queryResponse.exceptions.length) {
+    exceptions = queryResponse.exceptions as SqlException[];
+  }
+  if (queryResponse.resultTable?.dataSchema?.columnNames?.length) {
+    columnList = queryResponse.resultTable.dataSchema.columnNames;
+    dataArray = queryResponse.resultTable.rows;
+  }
+
+  return {
+    exceptions: exceptions,
+    result: {
+      columns: columnList,
+      records: dataArray,
+    },
+    queryStats: extractQueryStatsFromResponse(queryResponse),
+    data: queryResponse,
+  };
+};
+
 // This method is used to display query output in tabular format as well as 
JSON format on query page
 // API: /:urlName (Eg: sql or pql)
 // Expected Output: {columns: [], records: []}
 const getQueryResults = (params) => {
   return getQueryResult(params).then(({ data }) => {
     let queryResponse = getAsObject(data);
+    return processBrokerResponse(queryResponse);
+  });
+};
 
-    let exceptions: SqlException[] = [];
-    let dataArray = [];
-    let columnList = [];
-    // if sql api throws error, handle here
-    if(typeof queryResponse === 'string'){
-      exceptions.push({errorCode: null, message: queryResponse});
-    }
-    // if sql api returns a structured error with a `code`, handle here
-    if (queryResponse && queryResponse.code) {
-      if (queryResponse.error) {
-        exceptions.push({errorCode: null, message: "Query failed with error 
code: " + queryResponse.code + " and error: " + queryResponse.error});
-      } else {
-        exceptions.push({errorCode: null, message: "Query failed with error 
code: " + queryResponse.code + " but no logs. Please see controller logs for 
error."});
-      }
-    }
-    if (queryResponse && queryResponse.exceptions && 
queryResponse.exceptions.length) {
-      exceptions = queryResponse.exceptions as SqlException[];
-    }
-    if (queryResponse.resultTable?.dataSchema?.columnNames?.length) {
-      columnList = queryResponse.resultTable.dataSchema.columnNames;
-      dataArray = queryResponse.resultTable.rows;
-    }
-
-    const columnStats = ['timeUsedMs',
-      'numDocsScanned',
-      'totalDocs',
-      'numServersQueried',
-      'numServersResponded',
-      'numSegmentsQueried',
-      'numSegmentsProcessed',
-      'numSegmentsMatched',
-      'numConsumingSegmentsQueried',
-      'numEntriesScannedInFilter',
-      'numEntriesScannedPostFilter',
-      'numGroupsLimitReached',
-      'numGroupsWarningLimitReached',
-      'partialResult',
-      'minConsumingFreshnessTimeMs',
-      'offlineThreadCpuTimeNs',
-      'realtimeThreadCpuTimeNs',
-      'offlineSystemActivitiesCpuTimeNs',
-      'realtimeSystemActivitiesCpuTimeNs',
-      'offlineResponseSerializationCpuTimeNs',
-      'realtimeResponseSerializationCpuTimeNs',
-      'offlineTotalCpuTimeNs',
-      'realtimeTotalCpuTimeNs'
-    ];
-
-    const partialResult = queryResponse.partialResult ?? 
queryResponse.partialResponse;
-
-    return {
-      exceptions: exceptions,
-      result: {
-        columns: columnList,
-        records: dataArray,
-      },
-      queryStats: {
-        columns: columnStats,
-        records: [[queryResponse.timeUsedMs, queryResponse.numDocsScanned, 
queryResponse.totalDocs, queryResponse.numServersQueried, 
queryResponse.numServersResponded,
-          queryResponse.numSegmentsQueried, 
queryResponse.numSegmentsProcessed, queryResponse.numSegmentsMatched, 
queryResponse.numConsumingSegmentsQueried,
-          queryResponse.numEntriesScannedInFilter, 
queryResponse.numEntriesScannedPostFilter, queryResponse.numGroupsLimitReached, 
queryResponse.numGroupsWarningLimitReached,
-          partialResult ?? '-', queryResponse.minConsumingFreshnessTimeMs,
-          queryResponse.offlineThreadCpuTimeNs, 
queryResponse.realtimeThreadCpuTimeNs,
-          queryResponse.offlineSystemActivitiesCpuTimeNs, 
queryResponse.realtimeSystemActivitiesCpuTimeNs,
-          queryResponse.offlineResponseSerializationCpuTimeNs, 
queryResponse.realtimeResponseSerializationCpuTimeNs,
-          queryResponse.offlineTotalCpuTimeNs, 
queryResponse.realtimeTotalCpuTimeNs]]
-      },
-      data: queryResponse,
-    };
+// This method processes timeseries query results
+// Uses the same processBrokerResponse as SQL queries since both return 
BrokerResponseNativeV2
+// API: /query/timeseries
+// Expected Output: {exceptions: [], result: {columns: [], records: []}, 
queryStats: {columns: [], records: []}, data: {}}
+const getTimeseriesQueryResults = (params) => {
+  return getTimeSeriesQueryResult(params).then(({ data }) => {
+    let queryResponse = getAsObject(data);
+    return processBrokerResponse(queryResponse);
   });
 };
 
@@ -1381,7 +1406,7 @@ const getPackageVersionsData = () => {
       packageName,
       String(version)
     ]);
-    
+
     return {
       columns: ['Package', 'Version'],
       records: records
@@ -1399,6 +1424,7 @@ export default {
   getQueryLogicalTablesList,
   getTableSchemaData,
   getQueryResults,
+  getTimeseriesQueryResults,
   getTenantTableData,
   allTableDetailsColumnHeader,
   getAllTableDetails,
@@ -1490,3 +1516,6 @@ export default {
   getConsumingSegmentsInfoData,
   getPackageVersionsData
 };
+
+// Named exports for shared constants and utilities
+export { QUERY_STATS_COLUMNS, processBrokerResponse };


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to