diff --git a/web/src/components/dashboards/PanelSchemaRenderer.vue b/web/src/components/dashboards/PanelSchemaRenderer.vue index e5bf09c5a6..81614c7218 100644 --- a/web/src/components/dashboards/PanelSchemaRenderer.vue +++ b/web/src/components/dashboards/PanelSchemaRenderer.vue @@ -1333,117 +1333,68 @@ export default defineComponent({ } }; - const handleNoData = (panelType: any) => { - const xAlias = panelSchema.value.queries[0].fields.x.map( - (it: any) => it.alias || [], - ); - const yAlias = panelSchema.value.queries[0].fields.y.map( - (it: any) => it.alias || [], - ); - const zAlias = panelSchema.value.queries[0].fields.z.map( - (it: any) => it.alias || [], - ); - - const firstRow = data.value[0]?.[0]; - - switch (panelType) { - case "area": - case "area-stacked": - case "bar": - case "h-bar": - case "stacked": - case "h-stacked": - case "line": - case "scatter": - case "gauge": { - return ( - data.value[0]?.length > 1 || - (xAlias.every((x: any) => getDataValue(firstRow, x) != null) && - yAlias.every((y: any) => getDataValue(firstRow, y) != null)) - ); - } - case "table": { - // For tables, simply check if there's any data in the array - return ( - data.value[0]?.length > 1 || - (data.value[0]?.length == 1 && - (xAlias.some((x: any) => getDataValue(firstRow, x) != null) || - yAlias.some((y: any) => getDataValue(firstRow, y) != null))) - ); - } - case "metric": { - return ( - data.value[0]?.length > 1 || - yAlias.every((y: any) => getDataValue(firstRow, y) != null) - ); - } - case "heatmap": { - return ( - data.value[0]?.length > 1 || - (xAlias.every((x: any) => getDataValue(firstRow, x) != null) && - yAlias.every((y: any) => getDataValue(firstRow, y) != null) && - zAlias.every((z: any) => getDataValue(firstRow, z) != null)) - ); - } - case "pie": - case "donut": { - return ( - data.value[0]?.length > 1 || - yAlias.every((y: any) => getDataValue(firstRow, y) != null) - ); - } - case "maps": - case "geomap": { - return true; - } - case "sankey": { - const source = panelSchema.value.queries[0].fields.source.alias; - const target = panelSchema.value.queries[0].fields.target.alias; - const value = panelSchema.value.queries[0].fields.value.alias; - return ( - data.value[0]?.length > 1 || - source.every((s: any) => getDataValue(firstRow, s) != null) || - target.every((t: any) => getDataValue(firstRow, t) != null) || - value.every((v: any) => getDataValue(firstRow, v) != null) - ); - } - default: - break; - } - }; - - // Compute the value of the 'noData' variable + // Compute the value of the 'noData' variable. + // Instead of re-scanning raw rows, this checks the conversion output + // (panelData) which the pipeline already computed — O(1) property access. const noData = computed(() => { - // if panel type is 'html' or 'markdown', return an empty string - if ( - panelSchema.value.type == "html" || - panelSchema.value.type == "markdown" || - panelSchema.value.type == "custom_chart" - ) { + const type = panelSchema.value.type; + + if (type === "html" || type === "markdown" || type === "custom_chart") { return ""; } - // Check if the queryType is 'promql' - if (panelSchema.value?.queryType == "promql") { - // Check if the 'filteredData' array has elements and every item has a non-empty 'result' array + + if (panelSchema.value?.queryType === "promql") { return filteredData.value?.length && filteredData.value.some((item: any) => item?.result?.length) - ? "" // Return an empty string if there is data - : "No Data"; // Return "No Data" if there is no data + ? "" + : "No Data"; } - // A rendered chart is already on screen — suppress "No Data" even if - // the raw data buffer is momentarily empty. This prevents a flicker on - // refresh where the executor resets state.data = [] before loading - // flips to true, transiently firing this computed with empty data. - if (panelData.value?.options?.series?.length > 0) { + const hasRawData = data.value?.length && data.value[0]?.length; + + // During data buffer reset (executor clears state.data before reload), + // suppress "No Data" only while still loading to avoid flicker. + // Once loading completes with empty data, show "No Data". + if (!hasRawData) { + return loading.value && panelData.value?.chartType ? "" : "No Data"; + } + + // Raw data exists but conversion hasn't produced output yet — wait. + if (!panelData.value?.chartType) { return ""; } - // The queryType is not 'promql' - return data.value.length && - data.value[0]?.length && - handleNoData(panelSchema.value.type) - ? "" - : "No Data"; // Return "No Data" if the 'data' array is empty, otherwise return an empty string + + // Table renders raw rows; the conversion returns { rows, columns }. + if (type === "table") { + return panelData.value?.rows?.length > 0 ? "" : "No Data"; + } + + // Maps/geomap handle their own empty state. + if (type === "geomap" || type === "maps") { + return ""; + } + + // Sankey returns { options: null } when data is invalid. + if (type === "sankey") { + return panelData.value?.options != null ? "" : "No Data"; + } + + // Metric/gauge series are always created by the converter + // (they use renderItem, not data arrays), so check the raw Y value. + if (type === "metric" || type === "gauge") { + const firstRow = data.value[0]?.[0]; + const yAlias = panelSchema.value.queries[0].fields.y.map( + (it: any) => it.alias || [], + ); + return yAlias.every((y: any) => getDataValue(firstRow, y) != null) + ? "" + : "No Data"; + } + + // For all other chart types (line, area, bar, scatter, heatmap, etc.), + // the series filter in the conversion pipeline already excluded series + // whose data is entirely null. Just check what survived. + return panelData.value?.options?.series?.length > 0 ? "" : "No Data"; }); // when the error changes, emit the error