diff --git a/src/common/meta/dashboards/v4/mod.rs b/src/common/meta/dashboards/v4/mod.rs index 347e36f09e..6099619b94 100644 --- a/src/common/meta/dashboards/v4/mod.rs +++ b/src/common/meta/dashboards/v4/mod.rs @@ -177,6 +177,10 @@ pub struct PanelConfig { #[serde(skip_serializing_if = "Option::is_none")] decimals: Option, #[serde(skip_serializing_if = "Option::is_none")] + top_results: Option, + #[serde(skip_serializing_if = "Option::is_none")] + top_results_others: Option, + #[serde(skip_serializing_if = "Option::is_none")] axis_width: Option, #[serde(skip_serializing_if = "Option::is_none")] axis_border_show: Option, diff --git a/web/src/components/dashboards/addPanel/ConfigPanel.vue b/web/src/components/dashboards/addPanel/ConfigPanel.vue index 0e82ce537f..9b5c1c6c3c 100644 --- a/web/src/components/dashboards/addPanel/ConfigPanel.vue +++ b/web/src/components/dashboards/addPanel/ConfigPanel.vue @@ -15,7 +15,7 @@ along with this program. If not, see . --> +
+ + +
+ + + + + Include an 'others' series for values outside the top results when + using breakdown fields. + + +
+ +
+ . . . dashboardPanelData.layout.currentQueryIndex ].config, 1, - 'weight_fixed' + 'weight_fixed', ) " :label="t('common.weight')" @@ -768,7 +882,7 @@ along with this program. If not, see . ({ unit: null, unit_custom: null, decimals: 2, + top_results: null, + top_results_others: false, axis_width: null, axis_border_show: false, legend_width: { diff --git a/web/src/utils/dashboard/convertSQLData.ts b/web/src/utils/dashboard/convertSQLData.ts index fd6a10fe3c..df09630448 100644 --- a/web/src/utils/dashboard/convertSQLData.ts +++ b/web/src/utils/dashboard/convertSQLData.ts @@ -83,6 +83,121 @@ export const convertSQLData = async ( : []; }; + // Step 1: Get the X-Axis key + const xAxisKeys = getXAxisKeys(); + + // Step 2: Get the Y-Axis key + const yAxisKeys = getYAxisKeys(); + + const zAxisKeys = getZAxisKeys(); + + const breakDownKeys = getBreakDownKeys(); + + /** + * Process the SQL data and convert it into a format suitable for the echarts + * library. + * + * This function takes in the raw data returned from the SQL query and the + * panel schema, and returns a processed data array that can be fed into the + * echarts library. + * + * Here's a step-by-step breakdown of what this function does: + * + * 1. It checks if the data is empty or if the first item in the array is not + * an object. If either condition is true, it returns an empty array. + * + * 2. It extracts the top_results and top_results_others values from the + * panel schema config. + * + * 3. If top_results is not enabled, it simply returns the inner data array + * without any modifications. + * + * 4. It extracts the breakdown key, y-axis key, and x-axis key from the + * panel schema. + * + * 5. It aggregates the y-axis values by breakdown, ignoring items without a + * breakdown key. This is done using the reduce() method. + * + * 6. It sorts the breakdown object by value in descending order and extracts + * the top keys based on the configured number of top results. This is done + * using the Object.entries() and sort() methods. + * + * 7. It initializes an empty result array and an empty others object. + * + * 8. It loops through the inner data array and checks if the breakdown value + * is in the top keys. If it is, it adds the item to the result array. If it's + * not, and top_results_others is enabled, it adds the item to the others + * object. + * + * 9. If top_results_others is enabled, it loops through the others object and + * adds the aggregated values to the result array. + * + * 10. Finally, it returns the result array. + * + * @param {any[]} data - The data returned from the SQL query. + * @param {any} panelSchema - The schema of the panel. + * @returns {any[]} - The processed data array. + */ + const processData = (data: any[], panelSchema: any): any[] => { + if (!data.length || !Array.isArray(data[0])) { + return []; + } + + const { top_results, top_results_others } = panelSchema.config; + const innerDataArray = data[0]; + if (!top_results || !breakDownKeys.length) { + return innerDataArray; + } + + const breakdownKey = breakDownKeys[0]; + const yAxisKey = yAxisKeys[0]; + const xAxisKey = xAxisKeys[0]; + + // Step 1: Aggregate y_axis values by breakdown, ignoring items without a breakdown key + const breakdown = innerDataArray.reduce((acc, item) => { + const breakdownValue = item[breakdownKey]; + const yAxisValue = item[yAxisKey]; + if (breakdownValue) { + acc[breakdownValue] = (acc[breakdownValue] || 0) + (+yAxisValue || 0); + } + return acc; + }, {}); + + // Step 2: Sort and extract the top keys based on the configured number of top results + const topKeys = Object.entries(breakdown) + .sort(([, a]: any, [, b]: any) => b - a) + .slice(0, top_results) + .map(([key]) => key); + + // Step 3: Initialize result array and others object for aggregation + const resultArray: any[] = []; + const othersObj: any = {}; + + innerDataArray.forEach((item) => { + const breakdownValue = item[breakdownKey]; + if (topKeys.includes(breakdownValue)) { + resultArray.push(item); + } else if (top_results_others) { + const xAxisValue = String(item[xAxisKey]); + othersObj[xAxisValue] = + (othersObj[xAxisValue] || 0) + (+item[yAxisKey] || 0); + } + }); + + // Step 4: Add 'others' aggregation to the result array if enabled + if (top_results_others) { + Object.entries(othersObj).forEach(([xAxisValue, yAxisValue]) => { + resultArray.push({ + [breakdownKey]: "others", + [xAxisKey]: xAxisValue, + [yAxisKey]: yAxisValue, + }); + }); + } + + return resultArray; + }; + const getMarkLineData = (panelSchema: any) => { return ( panelSchema?.config?.mark_line?.map((markLine: any) => { @@ -101,6 +216,9 @@ export const convertSQLData = async ( }; const noValueConfigOption = panelSchema.config?.no_value_replacement; + + const processedData = processData(searchQueryData, panelSchema); + const missingValue = () => { // Get the interval in minutes const interval = resultMetaData.value.map( @@ -112,7 +230,7 @@ export const convertSQLData = async ( !metadata.queries || !["area-stacked", "line", "area"].includes(panelSchema.type) ) { - return JSON.parse(JSON.stringify(searchQueryData[0])); + return JSON.parse(JSON.stringify(processedData)); } // Extract and process metaDataStartTime @@ -127,7 +245,7 @@ export const convertSQLData = async ( const intervalMillis = interval * 1000; // Identify the time-based key - const searchQueryDataFirstEntry = searchQueryData[0][0]; + const searchQueryDataFirstEntry = processedData[0]; const keys = [ ...getXAxisKeys(), @@ -140,7 +258,7 @@ export const convertSQLData = async ( ); if (!timeBasedKey) { - return JSON.parse(JSON.stringify(searchQueryData[0])); + return JSON.parse(JSON.stringify(processedData)); } // Extract and process metaDataEndTime @@ -162,14 +280,14 @@ export const convertSQLData = async ( // Create a set of unique xAxis values const uniqueXAxisValues = new Set( - searchQueryData[0].map((d: any) => d[uniqueKey]), + processedData.map((d: any) => d[uniqueKey]), ); const filledData: any = []; let currentTime = binnedDate; // Create a map of existing data const searchDataMap = new Map(); - searchQueryData[0]?.forEach((d: any) => { + processedData?.forEach((d: any) => { const key = xAxisKeysWithoutTimeStamp.length > 0 || breakdownAxisKeysWithoutTimeStamp.length > 0 @@ -265,16 +383,6 @@ export const convertSQLData = async ( return result; }; - // Step 1: Get the X-Axis key - const xAxisKeys = getXAxisKeys(); - - // Step 2: Get the Y-Axis key - const yAxisKeys = getYAxisKeys(); - - const zAxisKeys = getZAxisKeys(); - - const breakDownKeys = getBreakDownKeys(); - const legendPosition = getLegendPosition( panelSchema.config?.legends_position, ); @@ -1015,12 +1123,12 @@ export const convertSQLData = async ( const key1 = breakDownKeys[0]; // get the unique value of the second xAxis's key const stackedXAxisUniqueValue = [ - ...new Set(searchQueryData[0].map((obj: any) => obj[key1])), + ...new Set(processedData.map((obj: any) => obj[key1])), ].filter((it) => it); options.series = stackedXAxisUniqueValue?.map((key: any) => { // queryData who has the xaxis[1] key as well from xAxisUniqueValue. - const data = searchQueryData[0].filter((it: any) => it[key1] == key); + const data = processedData.filter((it: any) => it[key1] == key); const seriesObj = { name: key, ...defaultSeriesProps, @@ -1224,12 +1332,12 @@ export const convertSQLData = async ( const key1 = breakDownKeys[0]; // get the unique value of the second xAxis's key const stackedXAxisUniqueValue = [ - ...new Set(searchQueryData[0].map((obj: any) => obj[key1])), + ...new Set(processedData.map((obj: any) => obj[key1])), ].filter((it) => it); options.series = stackedXAxisUniqueValue?.map((key: any) => { // queryData who has the xaxis[1] key as well from xAxisUniqueValue. - const data = searchQueryData[0].filter((it: any) => it[key1] == key); + const data = processedData.filter((it: any) => it[key1] == key); const seriesObj = { name: key, ...defaultSeriesProps,