From a0137c69de1c1040d6ffb0854c297604cc4386d6 Mon Sep 17 00:00:00 2001
From: Ole Henrik Flesaker <ohf@gapit.io>
Date: Thu, 27 Apr 2023 20:26:24 +0200
Subject: [PATCH] feat: add search by filter (#35)

* fix: fix return type

* feat: add method to get value by multiple filters

* chore: update changelog

* Update src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts

Co-authored-by: Ole Kristian (Zee) <ole.kristian.kaaseth@gmail.com>

* Update src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts

Co-authored-by: Ole Kristian (Zee) <ole.kristian.kaaseth@gmail.com>

* Update CHANGELOG.md

Co-authored-by: Ole Kristian (Zee) <ole.kristian.kaaseth@gmail.com>

* Update src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts

Co-authored-by: Ole Kristian (Zee) <ole.kristian.kaaseth@gmail.com>

* Update src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts

Co-authored-by: Ole Kristian (Zee) <ole.kristian.kaaseth@gmail.com>

* refac: add noDataValue & remove excessive type check

* feat: add reducerId to getMetricValueWithFilter

* fix: change import to correct reducerid

---------

Co-authored-by: Ole Kristian (Zee) <ole.kristian.kaaseth@gmail.com>
---
 CHANGELOG.md                                  |   6 +
 src/index.ts                                  |   1 +
 src/utils/getDataFieldsFromName.ts            |   2 +-
 .../getMetricValueWithFilters.test.ts         | 227 ++++++++++++++++++
 .../getMetricValueWithFilters.ts              | 123 ++++++++++
 src/utils/getMetricValueWithFilters/index.ts  |   1 +
 6 files changed, 359 insertions(+), 1 deletion(-)
 create mode 100644 src/utils/getMetricValueWithFilters/getMetricValueWithFilters.test.ts
 create mode 100644 src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts
 create mode 100644 src/utils/getMetricValueWithFilters/index.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8c6710..a52adf6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog for grafana-metric
 
+## Unreleased
+
+### Features / enhancements
+
+- Add method to filter by series name, field name and multiple labels [#35](https://github.com/gapitio/grafana-metric/pull/35)
+
 ## v1.1.1 (2022/01/07)
 
 ### Features / enhancements
diff --git a/src/index.ts b/src/index.ts
index e580b43..02e1f59 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,3 +5,4 @@ export * from "./utils/getDataFieldsFromName";
 export * from "./utils/getFieldFromName";
 export * from "./utils/getSeriesFromName";
 export * from "./utils/evaluateString";
+export * from "./utils/getMetricValueWithFilters";
diff --git a/src/utils/getDataFieldsFromName.ts b/src/utils/getDataFieldsFromName.ts
index ea0645b..e6c0766 100644
--- a/src/utils/getDataFieldsFromName.ts
+++ b/src/utils/getDataFieldsFromName.ts
@@ -77,7 +77,7 @@ function getSeriesAndValueField(
 export function getDataFieldsFromName(
   name: string,
   { searchLabels = true, getTime = true }: DataFieldOptions = {}
-): DataFields | Record<string, never> {
+): DataFields {
   const { series, valueField } = getSeriesAndValueField(name, { searchLabels });
   if (series && valueField)
     return {
diff --git a/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.test.ts b/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.test.ts
new file mode 100644
index 0000000..a398b30
--- /dev/null
+++ b/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.test.ts
@@ -0,0 +1,227 @@
+import { FieldType, LoadingState, PanelData, dateTime } from "@grafana/data";
+
+import { TIME_FIELD, field } from "../field";
+
+import { getMetricValueWithFilters } from "./getMetricValueWithFilters";
+
+declare global {
+  interface Window {
+    data?: PanelData;
+  }
+}
+
+window.data = {
+  state: LoadingState.Done,
+  series: [
+    {
+      name: "series-1",
+      fields: [
+        TIME_FIELD,
+        field({
+          name: "Value",
+          type: FieldType.number,
+          values: [47, 100],
+          labels: { label1: "label1" },
+        }),
+      ],
+      length: 1,
+    },
+    {
+      name: "series-2",
+      fields: [
+        TIME_FIELD,
+        field({
+          name: "Value",
+          type: FieldType.number,
+          values: [200],
+          labels: { label2: "label2" },
+        }),
+      ],
+      length: 1,
+    },
+    {
+      name: "series-3",
+      fields: [
+        TIME_FIELD,
+        field({
+          name: "FieldName",
+          type: FieldType.number,
+          values: [300],
+          labels: { label3: "label3" },
+        }),
+      ],
+      length: 1,
+    },
+    {
+      name: "series-3",
+      fields: [
+        TIME_FIELD,
+        field({
+          name: "FieldName",
+          type: FieldType.number,
+          values: [400],
+          labels: { label3: "label3", label4: "first" },
+        }),
+      ],
+      length: 1,
+    },
+    {
+      name: "series-3",
+      fields: [
+        TIME_FIELD,
+        field({
+          name: "FieldName",
+          type: FieldType.number,
+          values: [500],
+          labels: { label3: "label3", label4: "second" },
+        }),
+      ],
+      length: 1,
+    },
+    {
+      name: "series-12",
+      fields: [
+        TIME_FIELD,
+        field({
+          name: "FieldName",
+          type: FieldType.number,
+          values: [100, 200, 300, 400, 500],
+          labels: { label3: "label3", label4: "second" },
+        }),
+      ],
+      length: 5,
+    },
+  ],
+  timeRange: {
+    from: dateTime(0),
+    to: dateTime(0),
+    raw: {
+      from: dateTime(0),
+      to: dateTime(0),
+    },
+  },
+};
+
+describe("getMetricValue", () => {
+  beforeEach(() => {
+    jest.spyOn(global.Math, "random").mockReturnValue(0.5);
+  });
+
+  afterEach(() => {
+    jest.spyOn(global.Math, "random").mockRestore();
+  });
+
+  it("retrieves random value", () => {
+    expect(
+      getMetricValueWithFilters({ seriesName: "series-1", showcase: true })
+    ).toEqual(500);
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-1",
+        showcase: true,
+        range: { min: 0, max: 10 },
+        decimals: 2,
+      })
+    ).toEqual(5);
+  });
+
+  it("retrieves metric value w/o set fieldName & labels", () => {
+    expect(getMetricValueWithFilters({ seriesName: "series-1" })).toEqual(100);
+  });
+
+  it("retrieves metric value w/o set fieldName", () => {
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-1",
+        labels: { label1: "label1" },
+      })
+    ).toEqual(100);
+  });
+
+  it("retrieves metric value with all variables", () => {
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-3",
+        fieldName: "FieldName",
+        labels: { label3: "label3" },
+      })
+    ).toEqual(300);
+  });
+
+  it("retrieves first object when multiple is available", () => {
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-3",
+        fieldName: "FieldName",
+        labels: { label3: "label3" },
+      })
+    ).toEqual(300);
+  });
+
+  it("retrieves null when no match is found", () => {
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-4",
+        fieldName: "FieldName",
+        labels: { label3: "label3" },
+      })
+    ).toEqual(null);
+  });
+
+  it("retrieves null when including existing and nonexisting labels", () => {
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-3",
+        fieldName: "FieldName",
+        labels: { label3: "label3", nonExistent: "nonExistent" },
+      })
+    ).toEqual(null);
+  });
+
+  it("retrieves 'No data' when including noDataValue = 'No data'", () => {
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-3",
+        fieldName: "FieldName",
+        labels: { label3: "label3", nonExistent: "nonExistent" },
+        noDataValue: "No data",
+      })
+    ).toEqual("No data");
+  });
+
+  it("retrieves correct metric value when multiple labels", () => {
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-3",
+        fieldName: "FieldName",
+        labels: { label3: "label3", label4: "first" },
+      })
+    ).toEqual(400);
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-3",
+        fieldName: "FieldName",
+        labels: { label3: "label3", label4: "second" },
+      })
+    ).toEqual(500);
+  });
+
+  it("retrieves correct metric value with reducerId", () => {
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-12",
+        fieldName: "FieldName",
+        labels: { label3: "label3" },
+        reducerID: "first",
+      })
+    ).toEqual(100);
+    expect(
+      getMetricValueWithFilters({
+        seriesName: "series-12",
+        fieldName: "FieldName",
+        labels: { label3: "label3" },
+        reducerID: "last",
+      })
+    ).toEqual(500);
+  });
+});
diff --git a/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts b/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts
new file mode 100644
index 0000000..6a637a0
--- /dev/null
+++ b/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts
@@ -0,0 +1,123 @@
+import { DataFrame, Field, PanelData } from "@grafana/data";
+
+import { ReducerID } from "../field";
+import { getValue } from "../metricValue";
+import { getShowcaseMetricValue } from "../metricValue/getShowcaseMetricValue";
+
+declare const data: PanelData;
+
+function getMultipleSeriesFromName(seriesName: string): DataFrame[] {
+  return data.series.filter((series) => series.name === seriesName);
+}
+
+function getValueFieldsFromSeries(series: DataFrame[], fieldName: string) {
+  return series
+    .map(({ fields }) =>
+      fields.find((field) =>
+        [field.name, field.state?.displayName].includes(fieldName)
+      )
+    )
+    .filter((x): x is Exclude<typeof x, undefined> => x !== undefined);
+}
+
+function getValueFieldsFromName(metricName: string, fieldName: string) {
+  const series = getMultipleSeriesFromName(metricName);
+  return getValueFieldsFromSeries(series, fieldName);
+}
+
+function getValueFieldFromLabels(
+  valueFields: Field[],
+  labels: Record<string, string>
+) {
+  return valueFields.find(({ labels: fieldLabels }) =>
+    Object.entries(labels).every(
+      ([k, v]) => fieldLabels && fieldLabels[k] === v
+    )
+  );
+}
+
+function getMetricSeries({
+  seriesName,
+  fieldName,
+  labels,
+  noDataValue,
+  reducerID,
+}: {
+  seriesName: string;
+  fieldName: string;
+  labels: Record<string, string>;
+  noDataValue: unknown;
+  reducerID: string;
+}) {
+  const valueFields = getValueFieldsFromName(seriesName, fieldName);
+  const valueField = getValueFieldFromLabels(valueFields, labels);
+  const value =
+    valueField?.state?.calcs?.[reducerID] ?? getValue(valueField, reducerID);
+  return value ?? noDataValue;
+}
+
+/**
+ * Function provides value from grafana queries based on given filters.
+ *
+ * @example
+ *
+ * getMetricValueWithFilters({seriesName: "series-1"}); // Returns first series with name "series-1".
+ *
+ * getMetricValueWithFilters({
+     seriesName: "series-1",
+     fieldName: "field-1",
+   }); // Returns first series with name "series-1" and fieldName "field-1".
+ *
+ * getMetricValueWithFilters({
+     seriesName: "series-1",
+     fieldName: "field-1",
+     labels: { someLabelKey: "label-1", anotherLabelKey: "label-2" },
+   }) // Returns first series with name "series-1", fieldName "field-1" and field contains matching labels.
+ *
+ * // Showcase
+ * getMetricValueWithFilters({ seriesName: "series-1", showcase: true }) // Returns a random value between 0 and 1000.
+ *
+ * getMetricValueWithFilters({
+    seriesName: "series-1",
+    showcase: true,
+    range: { min: 0, max: 10 },
+    decimals: 2,
+  }) // Returns random value between 1-10.
+ *
+ * @param seriesName - String for identifying correct series in Grafana's data object.
+ * @param fieldName - String for identifying correct field in series(Defaults to "Value").
+ * @param labels - Object for filtering a specific series when multiple series have the same seriesName.
+ * @param showcase - Boolean for returning random showcase value.
+ * @param range - Object for setting minimum(min:) or maximum(max:) range for showcase value.
+ * @param decimals - Number for setting static amount of decimals for showcase value.
+ */
+export function getMetricValueWithFilters({
+  seriesName,
+  fieldName = "Value",
+  labels = {},
+  showcase,
+  range,
+  decimals,
+  noDataValue = null,
+  reducerID = ReducerID.last,
+}: {
+  seriesName: string;
+  fieldName?: string;
+  labels?: Record<string, string>;
+  showcase?: boolean;
+  range?: { min: number; max: number };
+  decimals?: number;
+  noDataValue?: unknown;
+  reducerID?: string;
+}): unknown {
+  if (showcase) {
+    return getShowcaseMetricValue({ range, decimals });
+  }
+  return getMetricSeries({
+    seriesName,
+    fieldName,
+    labels,
+    noDataValue,
+    reducerID,
+  });
+}
diff --git a/src/utils/getMetricValueWithFilters/index.ts b/src/utils/getMetricValueWithFilters/index.ts
new file mode 100644
index 0000000..90fcca6
--- /dev/null
+++ b/src/utils/getMetricValueWithFilters/index.ts
@@ -0,0 +1 @@
+export * from "./getMetricValueWithFilters";