Skip to content

Conversation

@ktx-abhay
Copy link
Collaborator

@ktx-abhay ktx-abhay commented Sep 5, 2025

PR Type

Enhancement


Description

  • Add dashboard schema version v6 with joins

  • Update backend to handle v6 dashboards

  • Extend panel builder for join-aware auto SQL

  • Migrate v5 dashboards to v6 schema


Diagram Walkthrough

flowchart LR
  v6schema["Add v6 Dashboard schema (joins, functions)"]
  backend["Backend models/controllers updated to v6"]
  ui["UI panel builder: join + field args"]
  migration["V5 -> V6 migration logic"]

  v6schema -- "From/Into super::Dashboard" --> backend
  backend -- "Expose v6 in APIs" --> ui
  ui -- "Build SQL with joins" --> sqlutils["SQL AST builder updates"]
  v6schema -- "Converters" --> migration
Loading

File Walkthrough

Relevant files
Enhancement
15 files
mod.rs
Introduce v6 dashboard schema with joins and rich fields 
+632/-0 
mod.rs
Wire v6 into unified Dashboard and accessors                         
+23/-1   
dashboards.rs
API models include v6 and parser routes                                   
+11/-2   
mod.rs
Support variables retrieval for v6 dashboards                       
+5/-1     
mod.rs
Super dashboard init updated to include v6 None                   
+1/-0     
mod.rs
Super dashboard init updated to include v6 None                   
+1/-0     
mod.rs
Super dashboard init updated to include v6 None                   
+1/-0     
mod.rs
Super dashboard init updated to include v6 None                   
+1/-0     
mod.rs
Super dashboard init updated to include v6 None                   
+1/-0     
useDashboardPanel.ts
Join-aware field selection and auto SQL generation             
+688/-516
sqlUtils.ts
Build SQL via AST with joins and functions                             
+384/-22
convertDashboardSchemaVersion.ts
Migrate v5 panels/filters to v6 field args                             
+152/-0 
convertSQLData.ts
Adapt conversions to v6 function fields                                   
+7/-7     
convertTableData.ts
Recognize histogram via functionName in v6                             
+2/-2     
FieldList.vue
Grouped fields per stream and filter actions                         
+164/-94
Formatting
1 files
SortByBtnGrp.vue
Styling tweaks and sort binding cleanup                                   
+19/-6   
Additional files
27 files
BuildFieldPopUp.vue +78/-0   
DashboardGeoMapsQueryBuilder.vue +135/-264
DashboardMapsQueryBuilder.vue +125/-219
DashboardQueryBuilder.vue +177/-728
DashboardSankeyChartBuilder.vue +156/-268
HistogramIntervalDropDown.vue +12/-8   
StreamFieldSelect.vue +225/-0 
DynamicFunctionPopUp.vue +279/-0 
RawQueryBuilder.vue +67/-0   
SelectFunction.vue +518/-0 
functionValidation.json +1039/-0
ViewPanel.vue +11/-12 
AscSort.vue +4/-4     
InnerJoinTypeSvg.vue +37/-0   
LeftJoinLineSvg.vue +20/-0   
LeftJoinSvg.vue +20/-0   
LeftJoinTypeSvg.vue +37/-0   
RightJoinLineSvg.vue +20/-0   
RightJoinSvg.vue +33/-0   
RightJoinTypeSvg.vue +37/-0   
SubTaskArrow.vue +29/-0   
en.json +1/-0     
convertDataIntoUnitValue.ts +936/-38
AddCondition.vue +76/-47 
AddJoinPopUp.vue +507/-0 
DashboardJoinsOption.vue +223/-0 
Group.vue +2/-2     

@github-actions
Copy link

github-actions bot commented Sep 5, 2025

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Schema Consistency

Many fields in v6 structs are private (no pub), but are consumed across crates and serialized/deserialized via APIs. Verify all fields that must round-trip through serde and be accessible externally are marked public and have correct serde rename/skip rules (e.g., PanelConfig fields like show_legends, legends_position) to avoid breakage in API responses and migrations.

#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize, ToSchema)]
pub struct PanelConfig {
    show_legends: bool,
    legends_position: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    unit: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    unit_custom: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    decimals: Option<OrdF64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    line_thickness: Option<OrdF64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    step_value: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    top_results: Option<OrdF64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    y_axis_min: Option<OrdF64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    y_axis_max: Option<OrdF64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    top_results_others: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    axis_width: Option<OrdF64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    axis_border_show: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    label_option: Option<LabelOption>,
    #[serde(skip_serializing_if = "Option::is_none")]
    show_symbol: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    line_interpolation: Option<LineInterpolation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    legend_width: Option<LegendWidth>,
    base_map: Option<BaseMap>,
    #[serde(skip_serializing_if = "Option::is_none")]
    map_type: Option<MapType>,
    map_view: Option<MapView>,
    #[serde(skip_serializing_if = "Option::is_none")]
    map_symbol_style: Option<MapSymbolStyle>,
    #[serde(skip_serializing_if = "Option::is_none")]
    drilldown: Option<Vec<DrillDown>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    mark_line: Option<Vec<MarkLine>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    override_config: Option<Vec<OverrideConfig>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    connect_nulls: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    no_value_replacement: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    wrap_table_cells: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    table_transpose: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    table_dynamic_columns: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    mappings: Option<Vec<Mapping>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    color: Option<ColorCfg>,
    #[serde(skip_serializing_if = "Option::is_none")]
    background: Option<Background>,
    #[serde(skip_serializing_if = "Option::is_none")]
    trellis: Option<Trellis>,
}
Possible Runtime Errors

New code frequently accesses nested properties like args and groupedFields without robust null checks and mutates arrays in-place. Ensure guards exist before using fields (e.g., itemY.args.length) and that watchers or async makeAutoSQLQuery handle race conditions with updateGroupedFields to prevent undefined access in UI.

  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.y.forEach((itemY: any) => {
    itemY.functionName = null;
    // take first arg
    itemY.args = itemY.args.length ? [itemY?.args?.[0]] : [];
  });
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.breakdown = [];
  dashboardPanelData.data.queries?.forEach((query: any) => {
    query.fields.latitude = null;
    query.fields.longitude = null;
    query.fields.weight = null;
    query.fields.name = null;
    query.fields.value_for_maps = null;
    query.fields.source = null;
    query.fields.target = null;
    query.fields.value = null;

    // make sure that x axis should not have more than one field
    if (query.fields.x.length > 1) {
      query.fields.x = [query.fields.x[0]];
    }

    // make sure that y axis should not have more than one field
    if (query.fields.y.length > 1) {
      query.fields.y = [query.fields.y[0]];
    }
  });
  if (dashboardPanelData.data.queryType === "sql") {
    dashboardPanelData.layout.currentQueryIndex = 0;
    dashboardPanelData.data.queries =
      dashboardPanelData.data.queries.slice(0, 1);
  }
  dashboardPanelData.data.htmlContent = "";
  dashboardPanelData.data.markdownContent = "";
  dashboardPanelData.data.customChartContent =
    getDefaultCustomChartText();
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].config.time_shift = [];
  break;

case "area":
case "area-stacked":
case "bar":
case "line":
case "scatter":
case "h-bar":
case "stacked":
case "h-stacked":
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.y.forEach((itemY: any) => {
    if (itemY.functionName === null && !itemY.isDerived) {
      itemY.functionName = "count";
      // take first arg
      itemY.args = itemY.args.length ? [itemY?.args?.[0]] : [];
    }
  });
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.z = [];
  // we have multiple queries for geomap, so if we are moving away, we need to reset
  // the values of lat, lng and weight in all the queries
  dashboardPanelData.data.queries?.forEach((query: any) => {
    query.fields.latitude = null;
    query.fields.longitude = null;
    query.fields.weight = null;
    query.fields.name = null;
    query.fields.value_for_maps = null;
    query.fields.source = null;
    query.fields.target = null;
    query.fields.value = null;

    // make sure that x axis should not have more than one field
    if (query.fields.x.length > 1) {
      // if breakdown is empty, then take 2nd x axis field on breakdown and remove all other x axis
      if (query.fields.breakdown.length === 0) {
        query.fields.breakdown = [query.fields.x[1]];
        query.fields.x = [query.fields.x[0]];
      } else {
        query.fields.x = [query.fields.x[0]];
      }
    }
  });
  if (dashboardPanelData.data.queryType === "sql") {
    dashboardPanelData.layout.currentQueryIndex = 0;
    dashboardPanelData.data.queries =
      dashboardPanelData.data.queries.slice(0, 1);
  }
  dashboardPanelData.data.htmlContent = "";
  dashboardPanelData.data.markdownContent = "";
  dashboardPanelData.data.customChartContent =
    getDefaultCustomChartText();
  break;
case "table":
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.y.forEach((itemY: any) => {
    if (itemY.functionName === null && !itemY.isDerived) {
      itemY.functionName = "count";
      // take first arg
      itemY.args = itemY.args.length ? [itemY?.args?.[0]] : [];
    }
  });
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.z = [];
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.breakdown = [];
  // we have multiple queries for geomap, so if we are moving away, we need to reset
  // the values of lat, lng and weight in all the queries
  dashboardPanelData.data.queries?.forEach((query: any) => {
    query.fields.latitude = null;
    query.fields.longitude = null;
    query.fields.weight = null;
    query.fields.name = null;
    query.fields.value_for_maps = null;
    query.fields.source = null;
    query.fields.target = null;
    query.fields.value = null;
  });
  if (dashboardPanelData.data.queryType === "sql") {
    dashboardPanelData.layout.currentQueryIndex = 0;
    dashboardPanelData.data.queries =
      dashboardPanelData.data.queries.slice(0, 1);
  }
  dashboardPanelData.data.htmlContent = "";
  dashboardPanelData.data.markdownContent = "";
  dashboardPanelData.data.customChartContent =
    getDefaultCustomChartText();

  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].config.time_shift = [];
  break;
case "pie":
case "donut":
case "gauge":
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.y.forEach((itemY: any) => {
    if (itemY.functionName === null && !itemY.isDerived) {
      itemY.functionName = "count";
      // take first arg
      itemY.args = itemY.args.length ? [itemY?.args?.[0]] : [];
    }
  });
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.z = [];
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.breakdown = [];
  // we have multiple queries for geomap, so if we are moving away, we need to reset
  // the values of lat, lng and weight in all the queries
  dashboardPanelData.data.queries?.forEach((query: any) => {
    query.fields.latitude = null;
    query.fields.longitude = null;
    query.fields.weight = null;
    query.fields.name = null;
    query.fields.value_for_maps = null;
    query.fields.source = null;
    query.fields.target = null;
    query.fields.value = null;

    // make sure that x axis should not have more than one field
    if (query.fields.x.length > 1) {
      query.fields.x = [query.fields.x[0]];
    }

    // make sure that y axis should not have more than one field
    if (query.fields.y.length > 1) {
      query.fields.y = [query.fields.y[0]];
    }
  });
  if (dashboardPanelData.data.queryType === "sql") {
    dashboardPanelData.layout.currentQueryIndex = 0;
    dashboardPanelData.data.queries =
      dashboardPanelData.data.queries.slice(0, 1);
  }
  dashboardPanelData.data.htmlContent = "";
  dashboardPanelData.data.markdownContent = "";
  dashboardPanelData.data.customChartContent =
    getDefaultCustomChartText();

  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].config.time_shift = [];
  break;
case "metric":
  dashboardPanelData.data.queries[
    dashboardPanelData.layout.currentQueryIndex
  ].fields.y.forEach((itemY: any) => {
    if (itemY.functionName === null && !itemY.isDerived) {
      itemY.functionName = "count";
      // take first arg
      itemY.args = itemY.args.length ? [itemY?.args?.[0]] : [];
    }
  });
  dashboardPanelData.data.queries[
Migration Robustness

v5→v6 migration mutates in place and assumes shapes (e.g., queryItem.stream vs queryItem.fields.stream). Confirm the correct source for stream alias, handle absent fields (z/breakdown may be undefined), and deep-clone or validate before mutation to avoid corrupting original objects or missing nested filter shapes.

    // update the version
    data.version = 5;
  }

  case 5: {
    // need to traverse all panels
    // for each panel
    //   for each query
    //      for each fields [x, y, z, breakdown, latitude, longitude, weight, source, target, value] Make sure that some of fields is not array
    //          add type: "build"
    //          field.column will go inside args array : {type: "field", value: field.column}
    data.tabs.forEach((tabItem: any) => {
      tabItem.panels.forEach((panelItem: any) => {
        panelItem.queries.forEach((queryItem: any) => {
          const {
            x,
            y,
            z,
            breakdown,
            latitude,
            longitude,
            weight,
            source,
            target,
            value,
          } = queryItem.fields;

          // Migrate all fields
          [
            x,
            y,
            z,
            breakdown,
            latitude,
            longitude,
            weight,
            source,
            target,
            value,
          ].forEach((field: any) => {
            migrateFields(
              field,
              queryItem.customQuery,
              queryItem.stream,
              migrateV5FieldsToV6,
            );
          });

          // Migrate the filters
          // all column which is currently string will be converted to object with streamAlias and field
          // make sure that conditions can be array based on filterType
          queryItem.fields.filter = migrateFilterConditions(
            queryItem.fields.filter,
          );
        });
      });
    });

    // update the version
    data.version = 6;
  }
}

@github-actions
Copy link

github-actions bot commented Sep 5, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Preserve argument types in AST

Non-field arguments (numbers, strings, nested functions) are coerced into
column_ref, which will produce invalid AST/SQL. Preserve argument types and
recursively build nested function expressions to avoid malformed SQL and runtime
errors.

web/src/utils/query/sqlUtils.ts [982-1006]

+const toExpr = (arg: any): any => {
+  if (!arg) return { type: "default", value: "unknown_column" };
+  switch (arg.type) {
+    case "field":
+      return {
+        type: "column_ref",
+        table: arg.value?.streamAlias || null,
+        column: arg.value?.field || "unknown_column",
+      };
+    case "number":
+      return { type: "number", value: arg.value ?? 0 };
+    case "string":
+    case "histogramInterval":
+      return { type: "string", value: String(arg.value ?? "") };
+    case "function":
+      return processField(arg.value)?.expr || { type: "default", value: "unknown_column" };
+    default:
+      return { type: "default", value: "unknown_column" };
+  }
+};
+
 const processFunctionArgs = (args: any[], isAggregation: boolean) => {
   if (isAggregation) {
-    // Aggregation functions need a single `expr` argument
     return {
       distinct: null,
-      expr: {
-        type: "column_ref",
-        table: null,
-        column: args[0]?.value?.field || args[0]?.value || "unknown_column",
-      },
+      expr: toExpr(args[0]),
       orderby: null,
       separator: null,
     };
   } else {
-    // Regular functions need an `expr_list`
     return {
       type: "expr_list",
-      value: args.map((arg) => ({
-        type: "column_ref",
-        table: null,
-        column: arg.value?.field || arg.value || "unknown_column",
-      })),
+      value: (args || []).map(toExpr),
     };
   }
 };
Suggestion importance[1-10]: 8

__

Why: Correctly modeling non-field arguments prevents malformed AST/SQL and aligns with nearby code that supports nested functions; this addresses a real bug with significant correctness impact.

Medium
Prevent async state races

Guard against race conditions by ensuring only the latest async result updates
groupedFields. Without cancellation/versioning, slower requests can overwrite newer
state, leading to wrong schema and filter formatting. Track a local token and only
apply results if still current.

web/src/composables/useDashboardPanel.ts [354-380]

+let groupedFieldsRequestId = 0;
+
 const updateGroupedFields = async () => {
   const currentStream =
     dashboardPanelData.data.queries[
       dashboardPanelData.layout.currentQueryIndex
     ].fields.stream;
   if (!currentStream) return;
 
-  // Collect streams (main + joins)
+  const requestId = ++groupedFieldsRequestId;
+
   const joinsStreams = [
     { stream: currentStream },
     ...(dashboardPanelData.data.queries[
       dashboardPanelData.layout.currentQueryIndex
     ].joins?.filter((stream: any) => stream?.stream) ?? []),
   ];
 
-  // Fetch stream fields
   const groupedFields = await Promise.all(
     joinsStreams.map(async (stream: any) => {
       return {
         ...(await loadStreamFields(stream?.stream)),
         stream_alias: stream?.streamAlias,
       };
     }),
   );
 
-  dashboardPanelData.meta.streamFields.groupedFields = groupedFields;
+  if (requestId === groupedFieldsRequestId) {
+    dashboardPanelData.meta.streamFields.groupedFields = groupedFields;
+  }
 };
Suggestion importance[1-10]: 7

__

Why: The proposal correctly guards against out-of-order async updates when loading grouped stream fields, which can cause incorrect schemas. It’s a solid reliability improvement, though not critical to core correctness.

Medium
Hide empty groups in filtered list

Group headers are kept even when no children match, causing empty groups to clutter
results. Filter groups to include only if themselves match or at least one child
matches. Preserve group rows only when they contain subsequent matching field rows.

web/src/components/dashboards/addPanel/FieldList.vue [1078-1099]

 const filterFieldFn = (rows: any, terms: any) => {
   if (!terms || terms.trim() === "") {
     return rows;
   }
-
   const searchTerm = terms.toLowerCase();
 
-  const filteredRows = rows.filter((row: any) => {
-    // Always include group headers
+  const result: any[] = [];
+  let currentGroup: any | null = null;
+  let groupHasMatch = false;
+
+  for (const row of rows) {
     if (row.isGroup) {
-      return true;
+      // If previous group had matches, push it (already pushed its children as we go)
+      if (currentGroup && groupHasMatch) {
+        // nothing to do, header already pushed
+      } else if (currentGroup && !groupHasMatch) {
+        // remove the previously pushed header as no children matched
+        result.pop();
+      }
+      // start new group
+      currentGroup = row;
+      groupHasMatch = false;
+      // only push header now; remove later if no children match
+      if (row.groupName?.toLowerCase().includes(searchTerm)) {
+        groupHasMatch = true;
+      }
+      result.push(row);
+      continue;
     }
 
-    // Filter fields based on name
-    return row.name.toLowerCase().includes(searchTerm);
-  });
+    // field row
+    const fieldMatches = row.name?.toLowerCase().includes(searchTerm);
+    if (fieldMatches) {
+      // ensure group header exists; if none, just push row
+      if (currentGroup && result[result.length - 1] !== currentGroup) {
+        result.push(currentGroup);
+      }
+      groupHasMatch = true;
+      result.push(row);
+    }
+  }
 
-  return filteredRows;
+  // finalize last group header if it had no matches and wasn't matching itself
+  if (currentGroup && !groupHasMatch && !currentGroup.groupName?.toLowerCase().includes(searchTerm)) {
+    // last pushed item is the header; remove it
+    if (result.length && result[result.length - 1] === currentGroup) {
+      result.pop();
+    }
+  }
+
+  return result;
 };
Suggestion importance[1-10]: 7

__

Why: The current filter keeps group headers even when no child matches; the proposal sensibly hides empty groups and improves UX without altering core logic. It’s accurate to the shown code and moderately improves readability of results.

Medium
Guard against null field labels

When field is null/undefined (no selection), accessing properties can throw and
labels render as "undefined". Add a defensive null check and return an empty string
or placeholder to prevent runtime errors and improve UX.

web/src/components/dashboards/addPanel/DashboardMapsQueryBuilder.vue [473-488]

 const commonBtnLabel = (field: any) => {
-  if (
-    dashboardPanelData.data.queries[
-      dashboardPanelData.layout.currentQueryIndex
-    ].customQuery
-  ) {
-    return field?.alias;
+  if (!field) return "";
+  const query = dashboardPanelData.data.queries[
+    dashboardPanelData.layout.currentQueryIndex
+  ];
+  if (query?.customQuery) {
+    return field?.alias || "";
   }
   return buildSQLQueryFromInput(
     field,
-    dashboardPanelData.data.queries[
-      dashboardPanelData.layout.currentQueryIndex
-    ]?.joins?.length
-      ? dashboardPanelData.data.queries[
-          dashboardPanelData.layout.currentQueryIndex
-        ].fields?.stream
-      : "",
-  );
+    (query?.joins?.length ? query?.fields?.stream : "") || ""
+  ) || "";
 };
Suggestion importance[1-10]: 6

__

Why: Adding null checks for field in commonBtnLabel is a safe defensive improvement that avoids "undefined" labels or runtime errors. Impact is moderate and aligns with the introduced function.

Low
Avoid silent failures on invalid functions

When a function is not found, silently returning an empty string can hide
misconfigurations and generate malformed SQL later. Surface a clear error indicator
so callers can handle it and prevent executing incomplete queries.

web/src/utils/dashboard/convertDataIntoUnitValue.ts [2101-2109]

 const selectedFunction = functionValidation.find(
   (fn: any) => fn.functionName === (functionName ?? null),
 );
 
-// If the function is not found, return empty string instead of throwing
+// If the function is not found, return an explicit indicator
 if (!selectedFunction) {
-  return "";
+  return "__INVALID_FUNCTION__";
 }
Suggestion importance[1-10]: 5

__

Why: Returning a sentinel like "INVALID_FUNCTION" makes failures explicit, but changes the function's contract (currently returns empty string) and could break callers; impact is moderate and context-dependent.

Low
General
Avoid duplicate field entries

Prevent duplicate field additions by restoring the existence checks you commented
out. Without this, repeated clicks add the same field multiple times, breaking query
generation and aliasing. Reintroduce a lookup that considers both field and
streamAlias.

web/src/composables/useDashboardPanel.ts [506-592]

 const addXAxisItem = (row: { name: string; streamAlias?: string }) => {
-  ...
+  if (
+    !dashboardPanelData.data.queries[
+      dashboardPanelData.layout.currentQueryIndex
+    ]?.fields?.x
+  ) {
+    return;
+  }
+
+  const isDerived = checkIsDerivedField(row.name) ?? false;
+
+  const alreadyExists = dashboardPanelData.data.queries[
+    dashboardPanelData.layout.currentQueryIndex
+  ].fields.x.some((it: any) => {
+    const fieldArg = Array.isArray(it.args) ? it.args.find((a: any) => a?.type === "field") : null;
+    return fieldArg?.value?.field === row.name && fieldArg?.value?.streamAlias === row.streamAlias;
+  });
+
+  if (alreadyExists) {
+    return;
+  }
+
   dashboardPanelData.data.queries[
     dashboardPanelData.layout.currentQueryIndex
   ].fields.x.push({
     label: !dashboardPanelData.data.queries[
       dashboardPanelData.layout.currentQueryIndex
     ].customQuery
       ? generateLabelFromName(row.name)
       : row.name,
     alias:
       !dashboardPanelData.data.queries[
         dashboardPanelData.layout.currentQueryIndex
       ].customQuery && !isDerived
         ? "x_axis_" +
           (dashboardPanelData.data.queries[
             dashboardPanelData.layout.currentQueryIndex
           ].fields.x.length +
             1)
         : row.name,
-    // column: row.name,
     color: null,
     type: "build",
     functionName:
       row.name == store.state.zoConfig.timestamp_column && !isDerived
         ? "histogram"
         : null,
     args:
       row.name == store.state.zoConfig.timestamp_column && !isDerived
         ? [
             {
               type: "field",
               value: {
                 field: row.name,
                 streamAlias: row.streamAlias,
               },
             },
             {
               type: "histogramInterval",
               value: null,
             },
           ]
         : [
             {
               type: "field",
               value: {
                 field: row.name,
                 streamAlias: row.streamAlias,
               },
             },
           ],
     sortBy:
       row.name == store.state.zoConfig.timestamp_column
         ? dashboardPanelData.data.type == "table"
           ? "DESC"
           : "ASC"
         : null,
     isDerived,
     havingConditions: [],
   });
-  ...
+
+  updateArrayAlias();
 };
Suggestion importance[1-10]: 8

__

Why: Restoring a de-duplication check for X-axis additions prevents repeated entries that can break query generation and aliasing. This aligns with the new args structure and has high practical impact on correctness.

Medium
Use safe join alias fallback

Using defaultStream as the alias fallback can create ambiguous references and
invalid SQL when defaultStream is empty or equals another alias. Ensure a safe alias
is always present by defaulting to the stream name when alias is missing.

web/src/utils/dashboard/convertDataIntoUnitValue.ts [2262-2265]

 export function buildSQLJoinsFromInput(
   joins: any[],
   defaultStream: any,
 ): string {
-  ...
-  joinClauses.push(
-    `${joinType.toUpperCase()} JOIN "${stream}" AS ${streamAlias ?? defaultStream} ON ${joinConditionsSQL}`,
-  );
-  ...
+  if (!joins || joins.length === 0) {
+    return "";
+  }
+
+  const joinClauses: string[] = [];
+
+  for (const join of joins) {
+    const { stream, streamAlias, joinType, conditions } = join;
+    if (!stream || !joinType || !conditions || conditions.length === 0) {
+      continue;
+    }
+
+    const joinConditionStrings: string[] = [];
+    for (const condition of conditions) {
+      const { leftField, rightField, operation } = condition;
+      if (!leftField?.field || !rightField?.field || !operation) continue;
+
+      const leftFieldStr = leftField.streamAlias
+        ? `${leftField.streamAlias}.${leftField.field}`
+        : defaultStream
+          ? `${defaultStream}.${leftField.field}`
+          : leftField.field;
+
+      const rightFieldStr = rightField.streamAlias
+        ? `${rightField.streamAlias}.${rightField.field}`
+        : defaultStream
+          ? `${defaultStream}.${rightField.field}`
+          : rightField.field;
+
+      joinConditionStrings.push(`${leftFieldStr} ${operation} ${rightFieldStr}`);
+    }
+
+    if (joinConditionStrings.length === 0) continue;
+
+    const joinConditionsSQL = joinConditionStrings.join(" AND ");
+    const safeAlias = streamAlias || stream; // fallback to stream name
+    joinClauses.push(
+      `${joinType.toUpperCase()} JOIN "${stream}" AS ${safeAlias} ON ${joinConditionsSQL}`,
+    );
+  }
+
+  return joinClauses.join(" ");
 }
Suggestion importance[1-10]: 6

__

Why: Falling back to the stream name for missing aliases reduces ambiguity versus using defaultStream, improving robustness; the change is small but beneficial.

Low
Stabilize unique join aliases

Assigning streamAlias as stream_${index} can change on reordering and collide across
edits. Generate a stable, unique alias per stream value (with numeric suffix) that
persists unless the stream changes. This prevents broken references in conditions
and field selections.

web/src/views/Dashboards/addPanel/DashboardJoinsOption.vue [171-189]

 watch(
   () =>
     dashboardPanelData.data.queries[
       dashboardPanelData.layout.currentQueryIndex
     ].joins,
   () => {
-    // generate alias for each join stream
-    // make sure that alias is unique
-    // if stream is duplicate then add _1, _2, _3 etc
-    dashboardPanelData.data.queries[
-      dashboardPanelData.layout.currentQueryIndex
-    ]?.joins?.forEach((join: any, index: number) => {
-      if (join?.stream) {
-        join.streamAlias = `stream_${index}`;
-      }
+    const query =
+      dashboardPanelData.data.queries[
+        dashboardPanelData.layout.currentQueryIndex
+      ];
+    const used: Record<string, number> = {};
+    query?.joins?.forEach((join: any) => {
+      if (!join?.stream) return;
+      const base = (join.streamAlias && join._lastStream === join.stream)
+        ? join.streamAlias.replace(/_\d+$/, "")
+        : `stream`;
+      // ensure stable alias per stream occurrence
+      used[base] = (used[base] || 0) + 1;
+      const alias = `${base}_${used[base]}`;
+      join.streamAlias = alias;
+      join._lastStream = join.stream;
     });
   },
-  {
-    deep: true,
-  },
+  { deep: true },
 );
Suggestion importance[1-10]: 5

__

Why: Making aliases stable across reorders can prevent reference churn, but the proposed logic introduces extra state (_lastStream) and assumptions. It’s a reasonable enhancement but not critical, and correctness depends on broader app behavior.

Low
Security
Harden condition building and operators

Validate logicalOperator positions to avoid undefined indices causing malformed SQL
like "AND undefined". Also, escape/quote condition.operator fallbacks to prevent
injection if unexpected values slip through. Add defaults and guard clauses.

web/src/composables/useDashboardPanel.ts [2324-2455]

 const buildCondition = (condition: any) => {
   const streamAlias =
-    condition.column.streamAlias ??
+    condition?.column?.streamAlias ??
     dashboardPanelData.data.queries[
       dashboardPanelData.layout.currentQueryIndex
     ].fields.stream;
 
-  if (condition.filterType === "group") {
-    const groupConditions = condition.conditions
-      .map(buildCondition)
-      .filter(Boolean);
-    const logicalOperators = condition.conditions
-      .map((c: any) => c.logicalOperator)
-      .filter(Boolean);
+  if (!condition) return "";
 
-    let groupQuery = "";
-    groupConditions.forEach((cond: any, index: any) => {
-      if (index > 0) {
-        groupQuery += ` ${logicalOperators[index]} `;
-      }
-      groupQuery += cond;
-    });
+  if (condition.filterType === "group" && Array.isArray(condition.conditions)) {
+    const built = condition.conditions.map(buildCondition).filter((c: any) => typeof c === "string" && c.length > 0);
+    if (!built.length) return "";
+    let groupQuery = built[0];
+    for (let i = 1; i < built.length; i++) {
+      const op = condition.conditions[i]?.logicalOperator || "AND";
+      groupQuery += ` ${op} ${built[i]}`;
+    }
+    return `(${groupQuery})`;
+  }
 
-    return groupConditions.length ? `(${groupQuery})` : "";
-  } else if (condition.type === "list" && condition.values?.length > 0) {
-    return `${streamAlias}.${condition.column.field} IN (${condition.values
+  if (!condition.column || !condition.column.field) return "";
+
+  if (condition.type === "list" && Array.isArray(condition.values) && condition.values.length > 0) {
+    const inVals = condition.values
       .map((value: any) => formatValue(value, condition.column))
-      .join(", ")})`;
-  } else if (condition.type === "condition" && condition.operator != null) {
+      .filter((v: any) => v !== null && v !== undefined)
+      .join(", ");
+    if (!inVals) return "";
+    return `${streamAlias}.${condition.column.field} IN (${inVals})`;
+  }
+
+  if (condition.type === "condition" && condition.operator != null) {
     let selectFilter = "";
+    const colExpr = `${streamAlias}.${condition.column.field}`;
+
     if (["Is Null", "Is Not Null"].includes(condition.operator)) {
-      selectFilter += `${streamAlias}.${condition.column.field} `;
-      switch (condition.operator) {
-        case "Is Null":
-          selectFilter += `IS NULL`;
-          break;
-        case "Is Not Null":
-          selectFilter += `IS NOT NULL`;
-          break;
-      }
-    } else if (condition.operator === "IN") {
-      selectFilter += `${streamAlias}.${condition.column.field} IN (${formatINValue(
-        condition.value,
-      )})`;
-    } else if (condition.operator === "NOT IN") {
-      selectFilter += `${streamAlias}.${condition.column.field} NOT IN (${formatINValue(
-        condition.value,
-      )})`;
-    } else if (condition.operator === "match_all") {
-      selectFilter += `match_all(${formatValue(condition.value, condition.column)})`;
-    } else if (condition.operator === "str_match") {
-      selectFilter += `str_match(${streamAlias}.${condition.column.field}, ${formatValue(
-        condition.value,
-        condition.column,
-      )})`;
-    } else if (condition.operator === "str_match_ignore_case") {
-      selectFilter += `str_match_ignore_case(${streamAlias}.${condition.column.field}, ${formatValue(condition.value, condition.column)})`;
-    } else if (condition.operator === "re_match") {
-      selectFilter += `re_match(${streamAlias}.${condition.column.field}, ${formatValue(
-        condition.value,
-        condition.column,
-      )})`;
-    } else if (condition.operator === "re_not_match") {
-      selectFilter += `re_not_match(${streamAlias}.${condition.column.field}, ${formatValue(
-        condition.value,
-        condition.column,
-      )})`;
-    } else if (condition.value != null && condition.value !== "") {
-      // streamAlias can be undefined or null, also, groupedfield will have one entry with streamAlias as null
-      // so we need to handle both cases
-      const streamFields = condition.column.streamAlias
-        ? dashboardPanelData?.meta?.streamFields?.groupedFields.find(
-            (it: any) => it.stream_alias === condition.column.streamAlias,
-          )
-        : dashboardPanelData?.meta?.streamFields?.groupedFields.find(
-            (it: any) =>
-              it.stream_alias === null || it.stream_alias === undefined,
-          );
+      selectFilter += `${colExpr} ${condition.operator === "Is Null" ? "IS NULL" : "IS NOT NULL"}`;
+      return selectFilter;
+    }
 
-      const columnType = streamFields?.schema?.find(
-        (it: any) => it.name == condition.column.field,
-      )?.type;
+    if (condition.operator === "IN" || condition.operator === "NOT IN") {
+      const v = formatINValue(condition.value);
+      if (!v) return "";
+      selectFilter += `${colExpr} ${condition.operator} (${v})`;
+      return selectFilter;
+    }
 
-      selectFilter += `${streamAlias}.${condition.column.field} `;
-      switch (condition.operator) {
-        case "=":
-        case "<>":
-        case "<":
-        case ">":
-        case "<=":
-        case ">=":
-          selectFilter += `${condition.operator} ${formatValue(
-            condition.value,
-            condition.column,
-          )}`;
-          break;
-        case "Contains":
-          selectFilter +=
-            columnType === "Utf8"
-              ? `LIKE '%${condition.value}%'`
-              : `LIKE %${condition.value}%`;
-          break;
-        case "Not Contains":
-          selectFilter +=
-            columnType === "Utf8"
-              ? `NOT LIKE '%${condition.value}%'`
-              : `NOT LIKE %${condition.value}%`;
-          break;
-        case "Starts With":
-          selectFilter +=
-            columnType === "Utf8"
-              ? `LIKE '${condition.value}%'`
-              : `LIKE ${condition.value}%`;
-          break;
-        case "Ends With":
-          selectFilter +=
-            columnType === "Utf8"
-              ? `LIKE '%${condition.value}'`
-              : `LIKE %${condition.value}`;
-          break;
-        default:
-          selectFilter += `${condition.operator} ${formatValue(
-            condition.value,
-            condition.column,
-          )}`;
-          break;
-      }
+    if (condition.operator === "match_all") {
+      const v = formatValue(condition.value, condition.column);
+      if (v === null || v === undefined) return "";
+      return `match_all(${v})`;
     }
-    return selectFilter;
+
+    if (condition.operator === "str_match") {
+      const v = formatValue(condition.value, condition.column);
+      if (v === null || v === undefined) return "";
+      return `str_match(${colExpr}, ${v})`;
+    }
+
+    if (condition.operator === "str_match_ignore_case") {
+      const v = formatValue(condition.value, condition.column);
+      if (v === null || v === undefined) return "";
+      return `str_match_ignore_case(${colExpr}, ${v})`;
+    }
+
+    if (condition.operator === "re_match") {
+      const v = formatValue(condition.value, condition.column);
+      if (v === null || v === undefined) return "";
+      return `re_match(${colExpr}, ${v})`;
+    }
+
+    if (condition.operator === "re_not_match") {
+      const v = formatValue(condition.value, condition.column);
+      if (v === null || v === undefined) return "";
+      return `re_not_match(${colExpr}, ${v})`;
+    }
+
+    if (condition.value == null || condition.value === "") return "";
+
+    const streamFields = condition.column.streamAlias
+      ? dashboardPanelData?.meta?.streamFields?.groupedFields.find(
+          (it: any) => it.stream_alias === condition.column.streamAlias,
+        )
+      : dashboardPanelData?.meta?.streamFields?.groupedFields.find(
+          (it: any) => it.stream_alias === null || it.stream_alias === undefined,
+        );
+
+    const columnType = streamFields?.schema?.find(
+      (it: any) => it.name == condition.column.field,
+    )?.type;
+
+    const safeOperator = ["=", "<>", "<", ">", "<=", ">=", "Contains", "Not Contains", "Starts With", "Ends With"].includes(condition.operator)
+      ? condition.operator
+      : "=";
+
+    switch (safeOperator) {
+      case "=":
+      case "<>":
+      case "<":
+      case ">":
+      case "<=":
+      case ">=":
+        return `${colExpr} ${safeOperator} ${formatValue(condition.value, condition.column)}`;
+      case "Contains":
+        return `${colExpr} ${columnType === "Utf8" ? `LIKE '%${condition.value}%'` : `LIKE %${condition.value}%`}`;
+      case "Not Contains":
+        return `${colExpr} ${columnType === "Utf8" ? `NOT LIKE '%${condition.value}%'` : `NOT LIKE %${condition.value}%`}`;
+      case "Starts With":
+        return `${colExpr} ${columnType === "Utf8" ? `LIKE '${condition.value}%'` : `LIKE ${condition.value}%`}`;
+      case "Ends With":
+        return `${colExpr} ${columnType === "Utf8" ? `LIKE '%${condition.value}'` : `LIKE %${condition.value}`}`;
+      default:
+        return `${colExpr} = ${formatValue(condition.value, condition.column)}`;
+    }
   }
   return "";
 };
Suggestion importance[1-10]: 6

__

Why: Adding null/array checks and default logical operators improves robustness and reduces malformed SQL fragments. However, full operator escaping/injection mitigation is limited and the change is moderate in impact.

Low

@ktx-abhay ktx-abhay force-pushed the feat/dashboard-join-for-auto branch 3 times, most recently from 6e24cfb to 20645df Compare September 15, 2025 08:31
@ktx-abhay ktx-abhay force-pushed the feat/dashboard-join-for-auto branch 5 times, most recently from 7355d24 to d346144 Compare October 13, 2025 04:35
@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: ktx-abhay | Branch: feat/dashboard-join-for-auto | Commit: 94f93bb

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
7 tests failed 114 94 7 10 3 82% 10m 59s

Test Failure Analysis

  1. sanity.spec.js: Multiple timeout issues related to element visibility and clicks
    1. Sanity Test Cases should display histogram in sql mode: Canvas element not found within timeout.
    2. Sanity Test Cases should display pagination when only SQL is on clicking and closing the result: Timestamp column locator click timed out.
    3. Sanity Test Cases should display pagination when histogram is off and clicking and closing the result: Source column locator click timed out.

Root Cause Analysis

  • The code changes in convertDashboardSchemaVersion.ts may have affected the visibility and availability of elements in the UI.

Recommended Actions

  1. Increase timeout durations for element visibility checks in sanity.spec.js tests. 2. Verify the presence of elements before performing actions to avoid timeouts. 3. Investigate the impact of recent changes in convertDashboardSchemaVersion.ts on UI rendering.

View Detailed Results

@testdino-playwright-reporter
Copy link

Test Run Failed

Author: ktx-abhay | Branch: feat/dashboard-join-for-auto | Commit: b535452

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
7 tests failed 30 20 7 3 0 67% 10m 59s

Test Failure Analysis

  1. sanity.spec.js: Multiple timeout issues related to element visibility and clicks
    1. Sanity Test Cases should display histogram in sql mode: Canvas element not found within timeout.
    2. Sanity Test Cases should display pagination when only SQL is on clicking and closing the result: Timestamp column locator click timed out.
    3. Sanity Test Cases should display pagination when histogram is off and clicking and closing the result: Source column locator click timed out.

Root Cause Analysis

  • The recent code changes in src/.../v6/mod.rs may have affected the rendering or availability of UI elements, leading to timeouts.

Recommended Actions

  1. Increase timeout values for element visibility checks in sanity.spec.js to accommodate slower rendering.
  2. Verify the presence of elements before performing actions in sanity.spec.js to avoid click timeouts.
  3. Ensure that the data required for rendering the histogram and pagination is loaded correctly before tests run.

View Detailed Results

@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: ktx-abhay | Branch: feat/dashboard-join-for-auto | Commit: 37f5c5a

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
7 tests failed 33 22 7 1 3 67% 4m 52s

Test Failure Analysis

  1. dashboard-filter.spec.js: Multiple timeout issues related to element visibility and interaction
    1. dashboard filter testcases Should apply the filter group inside group - robust version: Element not an input, fill action failed.
    2. dashboard filter testcases Should Filter work correctly if Added the breakdown field: Timeout waiting for cell to be visible.
    3. dashboard filter testcases Should display an error message if added the invalid operator: Click action timed out waiting for element.
  2. dashboard-transpose.spec.js: Timeout issue when waiting for element visibility
    1. dashboard UI testcases should not show an error when transpose is enabled from config with custom SQL query: Timeout waiting for remove button to be visible.

Root Cause Analysis

  • The changes in web/.../alerts/[VUE] FilterGroup.vue and related files may have introduced new conditions affecting element visibility and interaction.

Recommended Actions

  1. Increase timeout durations in dashboard-filter.spec.js for elements that are slow to appear. 2. Verify the visibility and state of elements before interactions in dashboard-filter.spec.js to avoid timeouts. 3. Ensure that the locator used in dashboard-transpose.spec.js correctly targets existing elements after recent code changes.

View Detailed Results

@ktx-abhay ktx-abhay force-pushed the feat/dashboard-join-for-auto branch from 9b8c39b to eeaa7f7 Compare October 14, 2025 08:36
@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: ktx-abhay | Branch: feat/dashboard-join-for-auto | Commit: eeaa7f7

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
3 tests failed 48 39 3 3 3 81% 5m 27s

Test Failure Analysis

  1. dashboard-geoMap.spec.js: Issues with element visibility and timeouts
    1. dashboard maps testcases Should display the correct location when manually entering latitude and longitude values: Locator timeout waiting for '#chart-map canvas'.
    2. dashboard maps testcases should correctly apply the filter conditions with different operators, and successfully apply them to the query: Element not an input; fill action failed.
  2. dashboard-multi-y-axis.spec.js: Timeout issues with element interactions
    1. dashboard multi y axis testcases should correctly display and update multiple Y-axes in edit panel.: Locator timeout waiting for '[data-test="dashboard-back-btn"]'.

Root Cause Analysis

  • The recent code changes in src/.../v6/mod.rs and web/.../dashboard/[JS] convertDashboardSchemaVersion.ts may have affected element visibility and interaction, leading to timeouts.

Recommended Actions

  1. Investigate the visibility and loading state of '#chart-map canvas' in dashboard-geoMap.spec.js to prevent timeouts.
  2. Ensure that the element '[data-test="dashboard-add-condition-column-0}"]' is an input type in dashboard-geoMap.spec.js to avoid fill errors.
  3. Review the timing of the back button's availability in dashboard-multi-y-axis.spec.js to address the timeout issue.

View Detailed Results

@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: ktx-abhay | Branch: feat/dashboard-join-for-auto | Commit: 9e579b4

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
2 tests failed 91 74 2 9 6 81% 4m 24s

Test Failure Analysis

  1. dashboard-geoMap.spec.js: Failures related to element interaction issues
    1. dashboard maps testcases Should display the correct location when manually entering latitude and longitude values: Locator timeout while waiting for map canvas to be clickable.
    2. dashboard maps testcases should correctly apply the filter conditions with different operators, and successfully apply them to the query: Attempted to fill a non-input element, causing an error.

Root Cause Analysis

  • The code change in convertDataIntoUnitValue.ts does not directly address the locator issues causing the test failures.

Recommended Actions

  1. Investigate the visibility and loading time of the '#chart-map canvas' element to reduce timeout errors. 2. Ensure the element '[data-test="dashboard-add-condition-column-0}"]' is an input type or adjust the test to interact with the correct element type.

View Detailed Results

@ktx-abhay ktx-abhay force-pushed the feat/dashboard-join-for-auto branch from 9e579b4 to fd2c761 Compare October 15, 2025 09:08
@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: ktx-abhay | Branch: feat/dashboard-join-for-auto | Commit: fd2c761

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
2 tests failed 91 75 2 9 5 82% 4m 24s

Test Failure Analysis

  1. dashboard-geoMap.spec.js: Failures due to element interaction issues
    1. dashboard maps testcases Should display the correct location when manually entering latitude and longitude values: Locator timeout waiting for canvas element to be clickable.
    2. dashboard maps testcases should correctly apply the filter conditions with different operators, and successfully apply them to the query: Attempted to fill a non-input element, causing an error.

Root Cause Analysis

  • The code change in convertDataIntoUnitValue.ts may have introduced validation issues affecting element interactions.

Recommended Actions

  1. Increase the timeout duration for the canvas locator in dashboard-geoMap.spec.js to prevent timeouts.
  2. Ensure the element targeted by '[data-test="dashboard-add-condition-column-0}"]' is an input or change the interaction method in dashboard-geoMap.spec.js.
  3. Review the element structure in the dashboard filter to confirm it supports fill actions.

View Detailed Results

@ktx-abhay ktx-abhay force-pushed the feat/dashboard-join-for-auto branch from fd2c761 to 5046f7e Compare October 27, 2025 05:27
@ktx-abhay ktx-abhay force-pushed the feat/dashboard-join-for-auto branch from 5046f7e to 77b1b99 Compare October 28, 2025 09:51
@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: ktx-abhay | Branch: feat/dashboard-join-for-auto | Commit: aba49a7

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
1 test failed 126 114 1 9 2 90% 4m 39s

Test Failure Analysis

  1. dashboard-streaming.spec.js: Timeout issues while interacting with UI elements
    1. dashboard streaming testcases should verify the custom value search from variable dropdown with streaming enabled: Locator timeout waiting for '[data-test="date-time-relative-tab"]' during click action.

Root Cause Analysis

  • The recent changes in dashboard-refresh.js may have affected the timing or availability of UI elements, leading to timeouts.

Recommended Actions

  1. Increase the timeout duration for the locator click in dashboard-refresh.js to accommodate slower UI responses. 2. Ensure the element '[data-test="date-time-relative-tab"]' is present and visible before attempting to click it. 3. Add explicit wait conditions before the click action to ensure the element is ready for interaction.

View Detailed Results

@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: ktx-abhay | Branch: feat/dashboard-join-for-auto | Commit: aba49a7

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
1 test failed 126 114 1 9 2 90% 4m 38s

Test Failure Analysis

  1. dashboard-streaming.spec.js: Timeout errors due to element visibility issues
    1. dashboard streaming testcases should verify the custom value search from variable dropdown with streaming enabled: Locator timeout waiting for '[data-test="dashboard-back-btn"]' to be visible.

Root Cause Analysis

  • The recent changes in dashboard-create.js may have affected the visibility of the back button, causing the timeout.

Recommended Actions

  1. Increase the timeout duration in backToDashboardList method in dashboard-create.js to allow more time for the button to become visible.
  2. Ensure that the button with '[data-test="dashboard-back-btn"]' is rendered correctly in the DOM before this test runs.
  3. Investigate any recent changes in the UI that could affect the rendering of the back button.

View Detailed Results

@testdino-playwright-reporter
Copy link

⚠️ Test Run Unstable


Author: ktx-abhay | Branch: feat/dashboard-join-for-auto | Commit: ed19ff4

Testdino Test Results

Status Total Passed Failed Skipped Flaky Pass Rate Duration
2 tests failed 92 79 2 9 2 86% 4m 26s

Test Failure Analysis

  1. dashboard-geoMap.spec.js: Failures due to element interaction issues
    1. dashboard maps testcases Should display the correct location when manually entering latitude and longitude values: Locator timeout waiting for '#chart-map canvas' to be clickable.
    2. dashboard maps testcases should correctly apply the filter conditions with different operators, and successfully apply them to the query: Attempted to fill a non-input element, causing an error.

Root Cause Analysis

  • The recent changes in the dashboard structure may have affected element locators and their expected types in the UI.

Recommended Actions

  1. Increase the timeout duration for the locator in dashboard-geoMap.spec.js to handle slow rendering. 2. Verify the element type for '[data-test="dashboard-add-condition-column-0}"]' in dashboard-filter.js to ensure it is an input type. 3. Review the changes in the dashboard structure to ensure all locators are correctly targeting existing elements.

View Detailed Results

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants