Repository: prometheus/prometheus (language: go)

Issue:
ui: fix handling of scalar and string in isHeatmapData
**Description:**
This PR fixes an edge case when handling scalar and string data in the `isHeatmapData` function.

Incidentally, another minor issue on the type of `DataTableProps['data']` (specifically for resultType == 'string') was discovered and fixed (picture attached below). 

Fixes #13292.

---

From here, it can be seen that for resultType == 'string', the type of `result` is not a string, but rather `SampleValue` (which is `[number, string]`)

<img width="869" alt="Screen Shot 2024-01-04 at 02 17 45" src="https://github.com/prometheus/prometheus/assets/69668484/bb8c5acd-7c89-4dd6-ad8f-39744e55db02">



Discussion / hints:
cc @bwplotka and @Loori-R from the issue thread.

And thanks to @machine424 for writing the initial test case!

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69ca76ca8ef..32758f15169 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Changelog
 
+
+## 2.49.0-rc.2 / 2024-01-08
+
+* [BUGFIX] UI: Fix scalar and string data type issues when querying introduced by heatmap feature. #13371
+
 ## 2.49.0-rc.1 / 2023-12-19
 
 * [BUGFIX] SD: Fix Azure VM Scale Set NIC issue. #13283
diff --git a/VERSION b/VERSION
index da758087aaa..05c0c2c410e 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-2.49.0-rc.1
+2.49.0-rc.2
diff --git a/web/ui/module/codemirror-promql/package.json b/web/ui/module/codemirror-promql/package.json
index a8933b8a585..92ad6b42ba3 100644
--- a/web/ui/module/codemirror-promql/package.json
+++ b/web/ui/module/codemirror-promql/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@prometheus-io/codemirror-promql",
-  "version": "0.49.0-rc.1",
+  "version": "0.49.0-rc.2",
   "description": "a CodeMirror mode for the PromQL language",
   "types": "dist/esm/index.d.ts",
   "module": "dist/esm/index.js",
@@ -29,7 +29,7 @@
   },
   "homepage": "https://github.com/prometheus/prometheus/blob/main/web/ui/module/codemirror-promql/README.md",
   "dependencies": {
-    "@prometheus-io/lezer-promql": "0.49.0-rc.1",
+    "@prometheus-io/lezer-promql": "0.49.0-rc.2",
     "lru-cache": "^7.18.3"
   },
   "devDependencies": {
diff --git a/web/ui/module/lezer-promql/package.json b/web/ui/module/lezer-promql/package.json
index ba2a1d1b279..2a27b18ef52 100644
--- a/web/ui/module/lezer-promql/package.json
+++ b/web/ui/module/lezer-promql/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@prometheus-io/lezer-promql",
-  "version": "0.49.0-rc.1",
+  "version": "0.49.0-rc.2",
   "description": "lezer-based PromQL grammar",
   "main": "dist/index.cjs",
   "type": "module",
diff --git a/web/ui/package-lock.json b/web/ui/package-lock.json
index 8e9b009c202..29597a88e0a 100644
--- a/web/ui/package-lock.json
+++ b/web/ui/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "prometheus-io",
-  "version": "0.49.0-rc.1",
+  "version": "0.49.0-rc.2",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "prometheus-io",
-      "version": "0.49.0-rc.1",
+      "version": "0.49.0-rc.2",
       "workspaces": [
         "react-app",
         "module/*"
@@ -30,10 +30,10 @@
     },
     "module/codemirror-promql": {
       "name": "@prometheus-io/codemirror-promql",
-      "version": "0.49.0-rc.1",
+      "version": "0.49.0-rc.2",
       "license": "Apache-2.0",
       "dependencies": {
-        "@prometheus-io/lezer-promql": "0.49.0-rc.1",
+        "@prometheus-io/lezer-promql": "0.49.0-rc.2",
         "lru-cache": "^7.18.3"
       },
       "devDependencies": {
@@ -70,7 +70,7 @@
     },
     "module/lezer-promql": {
       "name": "@prometheus-io/lezer-promql",
-      "version": "0.49.0-rc.1",
+      "version": "0.49.0-rc.2",
       "license": "Apache-2.0",
       "devDependencies": {
         "@lezer/generator": "^1.5.1",
@@ -20844,7 +20844,7 @@
     },
     "react-app": {
       "name": "@prometheus-io/app",
-      "version": "0.49.0-rc.1",
+      "version": "0.49.0-rc.2",
       "dependencies": {
         "@codemirror/autocomplete": "^6.11.1",
         "@codemirror/commands": "^6.3.2",
@@ -20862,7 +20862,7 @@
         "@lezer/lr": "^1.3.14",
         "@nexucis/fuzzy": "^0.4.1",
         "@nexucis/kvsearch": "^0.8.1",
-        "@prometheus-io/codemirror-promql": "0.49.0-rc.1",
+        "@prometheus-io/codemirror-promql": "0.49.0-rc.2",
         "bootstrap": "^4.6.2",
         "css.escape": "^1.5.1",
         "downshift": "^7.6.2",
@@ -23551,7 +23551,7 @@
         "@lezer/lr": "^1.3.14",
         "@nexucis/fuzzy": "^0.4.1",
         "@nexucis/kvsearch": "^0.8.1",
-        "@prometheus-io/codemirror-promql": "0.49.0-rc.1",
+        "@prometheus-io/codemirror-promql": "0.49.0-rc.2",
         "@testing-library/react-hooks": "^7.0.2",
         "@types/enzyme": "^3.10.18",
         "@types/flot": "0.0.36",
@@ -23602,7 +23602,7 @@
         "@lezer/common": "^1.1.1",
         "@lezer/highlight": "^1.2.0",
         "@lezer/lr": "^1.3.14",
-        "@prometheus-io/lezer-promql": "0.49.0-rc.1",
+        "@prometheus-io/lezer-promql": "0.49.0-rc.2",
         "isomorphic-fetch": "^3.0.0",
         "lru-cache": "^7.18.3",
         "nock": "^13.4.0"
diff --git a/web/ui/package.json b/web/ui/package.json
index cbbf0d7eac3..11a4c1ab12b 100644
--- a/web/ui/package.json
+++ b/web/ui/package.json
@@ -28,5 +28,5 @@
     "ts-jest": "^29.1.1",
     "typescript": "^4.9.5"
   },
-  "version": "0.49.0-rc.1"
+  "version": "0.49.0-rc.2"
 }
diff --git a/web/ui/react-app/package.json b/web/ui/react-app/package.json
index 72d04503183..ca94d8ed117 100644
--- a/web/ui/react-app/package.json
+++ b/web/ui/react-app/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@prometheus-io/app",
-  "version": "0.49.0-rc.1",
+  "version": "0.49.0-rc.2",
   "private": true,
   "dependencies": {
     "@codemirror/autocomplete": "^6.11.1",
@@ -19,7 +19,7 @@
     "@lezer/lr": "^1.3.14",
     "@nexucis/fuzzy": "^0.4.1",
     "@nexucis/kvsearch": "^0.8.1",
-    "@prometheus-io/codemirror-promql": "0.49.0-rc.1",
+    "@prometheus-io/codemirror-promql": "0.49.0-rc.2",
     "bootstrap": "^4.6.2",
     "css.escape": "^1.5.1",
     "downshift": "^7.6.2",
diff --git a/web/ui/react-app/src/pages/graph/DataTable.tsx b/web/ui/react-app/src/pages/graph/DataTable.tsx
index 901d7bb48d7..1885c7a80f6 100644
--- a/web/ui/react-app/src/pages/graph/DataTable.tsx
+++ b/web/ui/react-app/src/pages/graph/DataTable.tsx
@@ -24,7 +24,7 @@ export interface DataTableProps {
       }
     | {
         resultType: 'string';
-        result: string;
+        result: SampleValue;
       };
   useLocalTime: boolean;
 }
diff --git a/web/ui/react-app/src/pages/graph/GraphHeatmapHelpers.ts b/web/ui/react-app/src/pages/graph/GraphHeatmapHelpers.ts
index d564959cb80..d8b249c308f 100644
--- a/web/ui/react-app/src/pages/graph/GraphHeatmapHelpers.ts
+++ b/web/ui/react-app/src/pages/graph/GraphHeatmapHelpers.ts
@@ -1,10 +1,12 @@
+import { DataTableProps } from './DataTable';
 import { GraphProps, GraphSeries } from './Graph';
 
-export function isHeatmapData(data: GraphProps['data']) {
-  if (!data?.result?.length || data?.result?.length < 2) {
+export function isHeatmapData(data: DataTableProps['data']) {
+  if (data?.resultType === 'scalar' || data?.resultType === 'string' || !data?.result?.length || data?.result?.length < 2) {
     return false;
   }
-  const result = data.result;
+  // Type assertion to prevent TS2349 error.
+  const result = data.result as GraphProps['data']['result'];
   const firstLabels = Object.keys(result[0].metric).filter((label) => label !== 'le');
   return result.every(({ metric }) => {
     const labels = Object.keys(metric).filter((label) => label !== 'le');


Test patch:
diff --git a/web/ui/react-app/src/pages/graph/DataTable.test.tsx b/web/ui/react-app/src/pages/graph/DataTable.test.tsx
index d6974cf9062..1252b5bfcf7 100755
--- a/web/ui/react-app/src/pages/graph/DataTable.test.tsx
+++ b/web/ui/react-app/src/pages/graph/DataTable.test.tsx
@@ -338,7 +338,7 @@ describe('DataTable', () => {
     const dataTableProps: DataTableProps = {
       data: {
         resultType: 'string',
-        result: 'string',
+        result: [1572098246.599, 'test'],
       },
       useLocalTime: false,
     };
@@ -346,7 +346,7 @@ describe('DataTable', () => {
     it('renders a string row', () => {
       const table = dataTable.find(Table);
       const rows = table.find('tr');
-      expect(rows.text()).toEqual('stringt');
+      expect(rows.text()).toEqual('stringtest');
     });
   });
 });
diff --git a/web/ui/react-app/src/pages/graph/GraphHeatmapHelpers.test.ts b/web/ui/react-app/src/pages/graph/GraphHeatmapHelpers.test.ts
new file mode 100644
index 00000000000..26c44be68b2
--- /dev/null
+++ b/web/ui/react-app/src/pages/graph/GraphHeatmapHelpers.test.ts
@@ -0,0 +1,66 @@
+import { DataTableProps } from './DataTable';
+import { isHeatmapData } from './GraphHeatmapHelpers';
+
+describe('GraphHeatmapHelpers', () => {
+  it('isHeatmapData should return false for scalar and string resultType', () => {
+    let data = {
+      resultType: 'scalar',
+      result: [1703091180.125, '1703091180.125'],
+    } as DataTableProps['data'];
+    expect(isHeatmapData(data)).toBe(false);
+
+    data = {
+      resultType: 'string',
+      result: [1704305680.332, '2504'],
+    } as DataTableProps['data'];
+    expect(isHeatmapData(data)).toBe(false);
+  });
+
+  it('isHeatmapData should return false for a vector and matrix if length < 2', () => {
+    let data = {
+      resultType: 'vector',
+      result: [
+        {
+          metric: {
+            __name__: 'my_gauge',
+            job: 'target',
+          },
+          value: [1703091180.683, '6'],
+        },
+      ],
+    } as DataTableProps['data'];
+    expect(isHeatmapData(data)).toBe(false);
+
+    data = {
+      resultType: 'matrix',
+      result: [
+        {
+          metric: {},
+          values: [[1703091180.683, '6']],
+        },
+      ],
+    } as DataTableProps['data'];
+    expect(isHeatmapData(data)).toBe(false);
+  });
+
+  it('isHeatmapData should return true for valid heatmap data', () => {
+    const data = {
+      resultType: 'matrix',
+      result: [
+        {
+          metric: {
+            le: '100',
+          },
+          values: [[1703091180.683, '6']],
+        },
+        {
+          metric: {
+            le: '1000',
+          },
+          values: [[1703091190.683, '6.1']],
+        },
+      ],
+    } as DataTableProps['data'];
+    expect(isHeatmapData(data)).toBe(true);
+  });
+});