Skip to content

Commit

Permalink
Merge pull request #21 from gapitio/feature/calculate-null-and-bool-o…
Browse files Browse the repository at this point in the history
…ptions

feat: add options to calculate bool and/or null
  • Loading branch information
ZuperZee authored Mar 5, 2021
2 parents fee0ae2 + 7a11df3 commit c8cdcba
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 12 deletions.
40 changes: 40 additions & 0 deletions src/utils/getType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Types, getType } from "./getType";

describe("getType", () => {
it("returns Null", () => {
expect(getType(null)).toEqual(Types.Null);
});
it("returns Undefined", () => {
expect(getType(undefined)).toEqual(Types.Undefined);
});
it("returns Boolean", () => {
expect(getType(false)).toEqual(Types.Boolean);
expect(getType(true)).toEqual(Types.Boolean);
});
it("returns String", () => {
expect(getType("test")).toEqual(Types.String);
expect(getType("10")).toEqual(Types.String);
expect(getType("undefined")).toEqual(Types.String);
expect(getType("null")).toEqual(Types.String);
});
it("returns Number", () => {
expect(getType(0)).toEqual(Types.Number);
expect(getType(1)).toEqual(Types.Number);
expect(getType(1.5)).toEqual(Types.Number);
expect(getType(1000)).toEqual(Types.Number);
expect(getType(-1000)).toEqual(Types.Number);
expect(getType(Infinity)).toEqual(Types.Number);
expect(getType(-Infinity)).toEqual(Types.Number);
});
it("returns Array", () => {
expect(getType([])).toEqual(Types.Array);
expect(getType([1, 2, 3, 4])).toEqual(Types.Array);
expect(getType(Array(5))).toEqual(Types.Array);
expect(getType(Array(5).fill(null))).toEqual(Types.Array);
expect(getType([{ a: 1 }])).toEqual(Types.Array);
});
it("returns Object", () => {
expect(getType({})).toEqual(Types.Object);
expect(getType({ a: 2 })).toEqual(Types.Object);
});
});
21 changes: 21 additions & 0 deletions src/utils/getType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Checks the type of value and returns it as a string
* This works with all types (null, undefined, object, ETC)
*
* @param value - Value to check the type of
*
* @returns the type of the value as a string
*/
export function getType(value: unknown): string {
return Object.prototype.toString.call(value).slice(8, -1);
}

export const enum Types {
Null = "Null",
Undefined = "Undefined",
Boolean = "Boolean",
String = "String",
Number = "Number",
Array = "Array",
Object = "Object",
}
17 changes: 10 additions & 7 deletions src/utils/metricData/getMetricData.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { getMetricDataFromExpression } from "./getMetricDataFromExpression";
import {
MetricData,
MetricDataFromNameOptions,
getMetricDataFromName,
} from "./getMetricDataFromName";
MetricDataFromExpressionOptions,
getMetricDataFromExpression,
} from "./getMetricDataFromExpression";
import { MetricData, getMetricDataFromName } from "./getMetricDataFromName";
import {
ShowcaseMetricDataOptions,
getShowcaseMetricData,
} from "./getShowcaseMetricData";

interface MetricDataOptions
extends MetricDataFromNameOptions,
extends MetricDataFromExpressionOptions,
ShowcaseMetricDataOptions {
/**
* Decides if values are randomly generated.
Expand All @@ -33,6 +32,7 @@ export function getMetricData(
showcaseCalcs,
timeRange,
decimals,
calculationOptions,
}: MetricDataOptions = {}
): MetricData {
if (showcase) {
Expand All @@ -43,7 +43,10 @@ export function getMetricData(
decimals,
});
} else if (metricName.includes('"') || metricName.includes("'")) {
return getMetricDataFromExpression(metricName, { reducerIDs });
return getMetricDataFromExpression(metricName, {
reducerIDs,
calculationOptions,
});
}

return getMetricDataFromName(metricName, { reducerIDs });
Expand Down
94 changes: 93 additions & 1 deletion src/utils/metricData/getMetricDataFromExpression.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FieldType, LoadingState, PanelData, dateTime } from "@grafana/data";

import { ReducerID, TIME_FIELD, field } from "../field";
import { ReducerID, TIME_FIELD, TIME_VALUES, field } from "../field";

import { getMetricDataFromExpression } from "./getMetricDataFromExpression";

Expand Down Expand Up @@ -66,6 +66,30 @@ window.data = {
],
length: 1,
},
{
name: "series-null",
fields: [
TIME_FIELD,
field({
name: "Value",
type: FieldType.number,
calcs: { [ReducerID.last]: null, [ReducerID.first]: null },
}),
],
length: 1,
},
{
name: "series-boolean",
fields: [
TIME_FIELD,
field({
name: "Value",
type: FieldType.number,
calcs: { [ReducerID.last]: true, [ReducerID.first]: false },
}),
],
length: 1,
},
],
timeRange: {
from: dateTime(0),
Expand Down Expand Up @@ -133,6 +157,74 @@ describe("getMetricDataFromExpression", () => {
});
});

describe("null values", () => {
it("calculate null", () => {
expect(
getMetricDataFromExpression("'series-null' + 2", {
calculationOptions: { shouldCalculateNull: true },
})
).toStrictEqual({
calcs: {
[ReducerID.last]: 2,
[ReducerID.first]: 2,
},
time: {
[ReducerID.first]: TIME_VALUES[0],
[ReducerID.last]: TIME_VALUES[TIME_VALUES.length - 1],
},
hasData: true,
});
});
it("don't calculate null", () => {
expect(getMetricDataFromExpression("'series-null' + 2")).toStrictEqual({
calcs: {
[ReducerID.last]: null,
[ReducerID.first]: null,
},
time: {
[ReducerID.first]: TIME_VALUES[0],
[ReducerID.last]: TIME_VALUES[TIME_VALUES.length - 1],
},
hasData: true,
});
});
});

describe("boolean values", () => {
it("calculate boolean", () => {
expect(
getMetricDataFromExpression("'series-boolean' + 2", {
calculationOptions: { shouldCalculateBoolean: true },
})
).toStrictEqual({
calcs: {
[ReducerID.last]: 3,
[ReducerID.first]: 2,
},
time: {
[ReducerID.first]: TIME_VALUES[0],
[ReducerID.last]: TIME_VALUES[TIME_VALUES.length - 1],
},
hasData: true,
});
});
it("don't calculate boolean", () => {
expect(getMetricDataFromExpression("'series-boolean' + 2")).toStrictEqual(
{
calcs: {
[ReducerID.last]: true,
[ReducerID.first]: false,
},
time: {
[ReducerID.first]: TIME_VALUES[0],
[ReducerID.last]: TIME_VALUES[TIME_VALUES.length - 1],
},
hasData: true,
}
);
});
});

describe("reducer ids", () => {
it("returns value based on reducerID", () => {
expect(
Expand Down
88 changes: 84 additions & 4 deletions src/utils/metricData/getMetricDataFromExpression.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { evaluateString } from "../evaluateString";
import { Types, getType } from "../getType";

import {
MetricData,
Expand All @@ -7,15 +8,70 @@ import {
getMetricDataFromName,
} from "./getMetricDataFromName";

export interface CalculationOptions {
/**
* Calculate null values
*
* @description
* If multiple metrics are in the same expression
* - If set to false and shouldCalculateBoolean is false. The first null/boolean metric is used.
* - If set to false and shouldCalculateBoolean is true. The first null metric is used.
* - If set to true. The expression is calculated.
*
* @example
* const metricExpression = "'null-series' + 2"
*
* {null: false};
* return null
*
* {null: true};
* return null + 2 // 2
*
* @default false
*
*/
shouldCalculateNull?: boolean;

/**
* Calculate boolean values.
*
* @description
* If multiple metrics are in the same expression
* - If set to false and shouldCalculateNull is false. The first boolean/null metric is used.
* - If set to false and shouldCalculateNull is true. The first boolean metric is used.
* - If set to true. The expression is calculated.
*
* @example
* const metricExpression = "'true-series' + 2"
*
* {bool: false};
* return true
*
* {bool: true};
* return true + 2 // 3
*
* @default false
*
*/
shouldCalculateBoolean?: boolean;
}

function getCalcs({
metricExpression,
metricsData,
calculationOptions = {
shouldCalculateNull: false,
shouldCalculateBoolean: false,
},
}: {
metricExpression: string;
metricsData: {
[key: string]: MetricData;
};
calculationOptions?: CalculationOptions;
}) {
const { shouldCalculateNull, shouldCalculateBoolean } = calculationOptions;

// Get all unique calcs keys
const calcKeys = [
...new Set(
Expand All @@ -28,18 +84,35 @@ function getCalcs({

return Object.fromEntries(
calcKeys.map((calcKey) => {
// Used when an expression shouldn't be calculated
let alternativeValue: null | boolean | undefined;

const expression = metricExpression.replace(
/["']([^"']*)["']/g,
(rawMetricName) => {
if (alternativeValue !== undefined) return "";

const metricName = rawMetricName.replace(/["']/g, "");
const metricData = metricsData[metricName];
const value = metricData.calcs[calcKey];

const valueType = getType(value);
if (
(valueType == Types.Boolean && !shouldCalculateBoolean) ||
(valueType == Types.Null && !shouldCalculateNull)
)
alternativeValue = value as boolean | null;

return String(value);
}
);

return [calcKey, evaluateString(expression)];
const value =
alternativeValue === undefined
? evaluateString(expression)
: alternativeValue;

return [calcKey, value];
})
);
}
Expand Down Expand Up @@ -75,7 +148,10 @@ function getTime({
);
}

export type MetricDataFromExpressionOptions = MetricDataFromNameOptions;
export interface MetricDataFromExpressionOptions
extends MetricDataFromNameOptions {
calculationOptions?: CalculationOptions;
}

/**
* Evaluates a metric expression
Expand All @@ -85,7 +161,7 @@ export type MetricDataFromExpressionOptions = MetricDataFromNameOptions;
*/
export function getMetricDataFromExpression(
metricExpression: string,
{ reducerIDs }: MetricDataFromExpressionOptions = {}
{ reducerIDs, calculationOptions }: MetricDataFromExpressionOptions = {}
): MetricData {
const metricNames = metricExpression.match(/["']([^"']*)["']/g);

Expand All @@ -101,7 +177,11 @@ export function getMetricDataFromExpression(

if (!metricsData) return { calcs: {}, time: {}, hasData: false };

const calcs = getCalcs({ metricExpression, metricsData });
const calcs = getCalcs({
metricExpression,
metricsData,
calculationOptions: calculationOptions,
});

const hasData = Object.keys(calcs).length > 0;

Expand Down

0 comments on commit c8cdcba

Please sign in to comment.