From b6c69ff870e56ab865db6dfb087252c4a5e8b499 Mon Sep 17 00:00:00 2001 From: Matthieu Patou Date: Thu, 18 Apr 2024 12:22:50 -0700 Subject: [PATCH] Fix upscalling of data The initial upscalling was kind of naive, it turns out that if you have a serie with only a few datapoints but a very large time range we would strech them and make some value appears when they don't really exist. --- .config/jest.config.js | 4 + jest.config.js | 5 +- package.json | 11 +- src/components/SimplePanel.tsx | 44 ++--- src/cubism_utils.ts | 112 ++++++++++-- src/misc_utils.ts | 12 +- src/tests/cubism_utils.test.ts | 315 +++++++++++++++++++++++++++++++-- src/tests/misc_utils.test.ts | 23 +++ webpack.config.ts | 24 +-- 9 files changed, 472 insertions(+), 78 deletions(-) create mode 100644 src/tests/misc_utils.test.ts diff --git a/.config/jest.config.js b/.config/jest.config.js index 94489cb..238f452 100644 --- a/.config/jest.config.js +++ b/.config/jest.config.js @@ -13,6 +13,10 @@ module.exports = { '\\.(css|scss|sass)$': 'identity-obj-proxy', 'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'), }, + watchPathIgnorePatterns: [ + '/node_modules/', // Ignore changes in node_modules directory + '/dist/', // Ignore changes in the dist directory + ], modulePaths: ['/src'], setupFilesAfterEnv: ['/jest-setup.js'], testEnvironment: 'jest-environment-jsdom', diff --git a/jest.config.js b/jest.config.js index 79fd52a..2bd8504 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,7 +2,10 @@ // generally used by snapshots, but can affect specific tests process.env.TZ = 'UTC'; -module.exports = { +exports = { // Jest configuration provided by Grafana scaffolding ...require('./.config/jest.config'), + coverageDirectory: 'coverage/jest', }; +exports.coveragePathIgnorePatterns = ['.config/jest-setup.js']; +module.exports = exports; diff --git a/package.json b/package.json index ba58328..656c6ce 100644 --- a/package.json +++ b/package.json @@ -6,17 +6,26 @@ "build": "webpack -c ./webpack.config.ts --env production", "dev": "webpack -w -c ./webpack.config.ts --env development", "test": "jest --watch --onlyChanged", - "test:ci": "jest --passWithNoTests --maxWorkers 4", + "test:ci": "jest --passWithNoTests --maxWorkers 4 --coverage", "typecheck": "tsc --noEmit", "lint": "eslint --cache --ignore-path ./.gitignore --ignore-path ./3rdparty/.eslintignore --ext .js,.jsx,.ts,.tsx .", "lint:fix": "npm run lint -- --fix", "e2e": "npm exec cypress install && npm exec grafana-e2e run", "e2e:update": "npm exec cypress install && npm exec grafana-e2e run --update-screenshots", + "copy:reports": "mkdir -p coverage/combined && cp coverage/cypress/coverage-final.json coverage/combined/from-cypress.json && cp coverage/jest/coverage-final.json coverage/combined/from-jest.json", + "combine:reports": "npx nyc merge coverage/combined && mv coverage.json .nyc_output/out.json", + "prereport:combined": "npm run combine:reports", + "report:combined": "npx nyc report --reporter text --reporter clover --reporter json --report-dir coverage", + "prereport:lcov": "npm run precombine:reports", + "report:lcov": "genhtml coverage/cypress/lcov.info coverage/jest/lcov.info --output-directory=coverage/lcov-report", "server": "docker-compose up --build", "sign": "npx --yes @grafana/sign-plugin@latest", "pretty": "prettier --single-quote --write \"{src,__{tests,demo,dist}__}/**/*.ts*\"", "prepare": "relative-deps" }, + "nyc": { + "report-dir": "coverage/cypress" + }, "author": "Matthieu Patou", "license": "Apache-2.0", "devDependencies": { diff --git a/src/components/SimplePanel.tsx b/src/components/SimplePanel.tsx index bbaa11e..be96d87 100644 --- a/src/components/SimplePanel.tsx +++ b/src/components/SimplePanel.tsx @@ -7,7 +7,7 @@ import * as cubism from 'cubism-es'; import * as d3 from 'd3'; import { config } from '@grafana/runtime'; -import { convertDataToCubism } from '../cubism_utils'; +import { convertAllDataToCubism} from '../cubism_utils'; import { log_debug } from '../misc_utils'; import { calculateSecondOffset } from '../date_utils'; @@ -21,15 +21,11 @@ interface Props extends PanelProps {} type CSS = string; -type Styles = { - wrapper: CSS; - d3inner: CSS; - d3outer: CSS; - svg: CSS; - textBox: CSS; +interface CSSStyles { + [ key: string ]: CSS }; -const getStyles = (showText: boolean): (() => Styles) => { +const getStyles = (showText: boolean): (() => CSSStyles) => { return function () { // 28px is the height of the axis let innerheight = 'calc(100% - 28px)'; @@ -38,27 +34,27 @@ const getStyles = (showText: boolean): (() => Styles) => { outerheight = 'calc(100% - 2em)'; } return { - wrapper: css` + 'wrapper': css` height: 100%; font-family: Open Sans; position: relative; overflow: hidden; `, - d3inner: css` + 'd3inner': css` overflow: auto; height: ${innerheight}; `, - d3outer: css` + 'd3outer': css` position: relative; height: ${outerheight}; overflow: hidden; `, - svg: css` + 'svg': css` position: absolute; top: 0; left: 0; `, - textBox: css` + 'textBox': css` max-height: 2em; `, }; @@ -71,7 +67,7 @@ export const D3Graph: React.FC<{ width: number; data: PanelData; options: SimpleOptions; - stylesGetter: Styles; + stylesGetter: CSSStyles; }> = ({ height, width, data, options, stylesGetter }) => { const theme = useTheme2(); let context = cubism.context(); @@ -107,10 +103,16 @@ export const D3Graph: React.FC<{ } log_debug('Step is:', step); log_debug('Length of timestamps is ', cubismTimestamps.length); + log_debug('Size of the graph is ', size); + + wrapperDiv.innerHTML = ''; + wrapperDiv.className = stylesGetter["wrapper"]; + if (data.series.length === 0) { + wrapperDiv.innerHTML = 'The series contained no data, check your query'; + return; + } + let cubismData = convertAllDataToCubism(data.series, cubismTimestamps, context, step); - let cubismData = data.series.map(function (series, seriesIndex) { - return convertDataToCubism(series, seriesIndex, cubismTimestamps, context); - }); cubismData = cubismData.filter(function (el) { if (el !== null) { return el; @@ -121,8 +123,6 @@ export const D3Graph: React.FC<{ now = Date.now(); log_debug(`Took ${now - prev} to convert the series`); - wrapperDiv.innerHTML = ''; - wrapperDiv.className = stylesGetter.wrapper; if (cubismData.length === 0) { wrapperDiv.innerHTML = 'The series contained no data, check your query'; @@ -132,11 +132,11 @@ export const D3Graph: React.FC<{ // setup Div layout and set classes const delta = calculateSecondOffset(begin, +(end), request.timezone, request.range.from.utcOffset()); const outerDiv = d3.create('div'); - outerDiv.node()!.className = stylesGetter.d3outer; + outerDiv.node()!.className = stylesGetter["d3outer"]; // TODO rename that to canvas const innnerDiv = d3.create('div'); const axisDiv = d3.create('div'); - innnerDiv.node()!.className = stylesGetter.d3inner; + innnerDiv.node()!.className = stylesGetter["d3inner"]; // setup the context // size is the nubmer of pixel @@ -226,7 +226,7 @@ export const D3Graph: React.FC<{ log_debug('showing text'); let msg = `${options.text}`; const msgDivContainer = d3.create('div'); - msgDivContainer.node()!.className = stylesGetter.textBox; + msgDivContainer.node()!.className = stylesGetter["textBox"]; msgDivContainer.append('div').text(msg); wrapperDiv.append(msgDivContainer.node()!); } diff --git a/src/cubism_utils.ts b/src/cubism_utils.ts index 60aa4bf..076cc07 100644 --- a/src/cubism_utils.ts +++ b/src/cubism_utils.ts @@ -1,7 +1,10 @@ -import _ from 'lodash'; import { DataFrame, getFieldDisplayName } from '@grafana/data'; +import { log_debug } from './misc_utils'; +import _ from 'lodash'; -export function upSampleData(dataPoints: number[], dataPointsTS: number[], pointIndex: number) { +// Take the datapoints of the timeseries and its associated timestamps +export function upSampleData(dataPoints: number[], dataPointsTS: number[]) { + let pointIndex = 0; return function (ts: number, tsIndex: number) { let point = dataPoints[pointIndex]; let nextPoint = null; @@ -35,7 +38,22 @@ export function downSampleData(timestamps: number[], dataAndTS: number[][], over }); if (values.length === 0) { - return val; + if (tsIndex === 0 || nextTs === null) { + return null; + } + // Potentially extrapolate some points but it's not clear why we should do that + let lastTS = timestamps[tsIndex - 1]; + values = dataAndTS + .filter(function (point) { + return point[0] >= lastTS && point[0] < ts; + }) + .map(function (point) { + return point[1]; + }); + + if (values.length === 0) { + return null; + } } if (override.summaryType === 'sum') { @@ -76,24 +94,27 @@ export function minValue(values: number[]) { }); } -export function convertDataToCubism(series: DataFrame, seriesIndex: number, timestamps: number[], context: any) { - if (series.length > 0) { - let name = getFieldDisplayName(series.fields[1], series); +// Take an array of timestamps that map to the way we want to display the timeseries in grafana +// take also a serie +export function convertDataToCubism( + serie: DataFrame, + serieIndex: number, + timestamps: number[], + context: any, + downSample: boolean +) { + if (serie.length > 0) { + let name = getFieldDisplayName(serie.fields[1], serie); return context.metric(function (start: number, stop: number, step: number, callback: any) { - let dataPoints: number[] = series.fields[1].values; - let dataPointsTS: number[] = series.fields[0].values; - let values: number[] = []; - if (timestamps.length === dataPoints.length) { - values = dataPoints.map(function (point: number) { - return point; - }); - } else if (timestamps.length > dataPoints.length) { - let pointIndex = 0; - values = _.chain(timestamps).map(upSampleData(dataPoints, dataPointsTS, pointIndex)).value(); - } else { - let override = { summaryType: 'avg' }; - let dataAndTS = dataPointsTS.map((item, index) => [item, dataPoints[index]]); + let dataPoints: number[] = serie.fields[1].values; + let dataPointsTS: number[] = serie.fields[0].values; + let values: Array = []; + let override = { summaryType: 'avg' }; + let dataAndTS = dataPointsTS.map((item, index) => [item, dataPoints[index]]); + if (downSample) { values = _.chain(timestamps).map(downSampleData(timestamps, dataAndTS, override)).value(); + } else { + values = _.chain(timestamps).map(upSampleData(dataPoints, dataPointsTS)).value(); } callback(null, values); }, name); @@ -101,3 +122,56 @@ export function convertDataToCubism(series: DataFrame, seriesIndex: number, time return null; } } + +export function convertAllDataToCubism(series: DataFrame[], cubismTimestamps: number[], context: any, step: number) { + let longest = series[0].length; + let longestIndex = 0; + + for (let i = 1; i < series.length; i++) { + if (series[i].length > longest) { + longest = series[i].length; + longestIndex = i; + } + } + // Let's look at the longest one, if the step is bigger than what we have in the serie we downsample + const name = 'Time'; + let s = series[longestIndex]; + let ts = s.fields.filter(function (v) { + return v.name === name ? true : false; + })[0]; + let previousts = -1; + let v: number[] = []; + if (ts === undefined) { + log_debug(`Couldn't find a field with name ${name} using field 0`); + ts = s.fields[0]; + } + log_debug(`There is ${ts.values.length} elements in the longest`); + for (let i = 0; i < ts.values.length; i++) { + if (previousts !== -1) { + v.push(ts.values[i] - previousts); + } + previousts = ts.values[i]; + } + v.sort((a: number, b: number) => a - b); + + // Calculate the index for P99 + const index = Math.ceil(0.99 * v.length) - 1; + // Look at what is the ratio when comparing the smallest step (ie. when we have most of the data + // to the largest (ie. when there is gaps), if the ratio is more than 3 we will + // downsample because it means that there is massive gaps + const stepRatio = v[index] / v[0]; + + let downsample = false; + // if there is too much missing points (ie. p99 has has at least 3x the smallest step then force + // downsample because upSample will not give good results + if (stepRatio > 3 || s.fields[0].values.length > cubismTimestamps.length) { + downsample = true; + } + log_debug( + `downsample = ${downsample} v[index] = ${v[index]} stepRatio = ${stepRatio} index = ${index}, step = ${step}` + ); + + return series.map(function (serie, serieIndex) { + return convertDataToCubism(serie, serieIndex, cubismTimestamps, context, downsample); + }); +} diff --git a/src/misc_utils.ts b/src/misc_utils.ts index b7332d1..272e3d9 100644 --- a/src/misc_utils.ts +++ b/src/misc_utils.ts @@ -1,8 +1,14 @@ +let debug = false; +export function enableDebug(): void { + debug = true; +} export function log_debug(message?: any, ...optionalParams: any[]): void { // Set it to true to enable debugging - let debug = false; if (debug) { - return console.log(message, optionalParams); + if (optionalParams.length > 0) { + return console.log(message, optionalParams.join(' ')); + } else { + return console.log(message); + } } } - diff --git a/src/tests/cubism_utils.test.ts b/src/tests/cubism_utils.test.ts index 46d0f21..3616b10 100644 --- a/src/tests/cubism_utils.test.ts +++ b/src/tests/cubism_utils.test.ts @@ -1,5 +1,6 @@ import { convertDataToCubism, + convertAllDataToCubism, upSampleData, downSampleData, sumValues, @@ -30,9 +31,54 @@ describe('convertDataToCubism', () => { metric: (callback: any, name: string) => {}, }; - expect(() => convertDataToCubism(series, seriesIndex, timestamps, context)).not.toThrow(); + expect(() => convertDataToCubism(series, seriesIndex, timestamps, context, true)).not.toThrow(); }); - it('should extend the data points', () => { + it('should return null if no serie', () => { + const input1 = { + target: 'Field Name', + datapoints: [], + }; + let series = toDataFrame(input1); + const seriesIndex = 0; + const timestamps = [1, 2, 4, 6, 7, 10]; + let values; + const context = { + metric: (callback: any, name: string) => { + callback(1, 20, 1, (a: any, b: any) => { + values = b; + }); + }, + }; + + let v = convertDataToCubism(series, seriesIndex, timestamps, context, false); + expect(values).toBe(undefined); + expect(v).toBe(null); + }); + it('should extend the data points when upsampling', () => { + const input1 = { + target: 'Field Name', + datapoints: [ + [100, 1], + [200, 4], + [1000, 10], + ], + }; + let series = toDataFrame(input1); + const seriesIndex = 0; + const timestamps = [1, 2, 4, 6, 7, 10]; + let values; + const context = { + metric: (callback: any, name: string) => { + callback(1, 20, 1, (a: any, b: any) => { + values = b; + }); + }, + }; + + convertDataToCubism(series, seriesIndex, timestamps, context, false); + expect(values).toStrictEqual([100, 100, 200, 200, 200, 1000]); + }); + it('should not extend the data points when downsampling', () => { const input1 = { target: 'Field Name', datapoints: [ @@ -43,7 +89,7 @@ describe('convertDataToCubism', () => { }; let series = toDataFrame(input1); const seriesIndex = 0; - const timestamps = [1, 2, 4, 6, 10]; + const timestamps = [1, 2, 4, 6, 7, 10]; let values; const context = { metric: (callback: any, name: string) => { @@ -53,8 +99,8 @@ describe('convertDataToCubism', () => { }, }; - convertDataToCubism(series, seriesIndex, timestamps, context); - expect(values).toStrictEqual([100, 100, 200, 200, 1000]); + convertDataToCubism(series, seriesIndex, timestamps, context, true); + expect(values).toStrictEqual([100, 100, 200, 200, null, 1000]); }); it('should return the same number of data points', () => { @@ -78,10 +124,13 @@ describe('convertDataToCubism', () => { }, }; - convertDataToCubism(series, seriesIndex, timestamps, context); + convertDataToCubism(series, seriesIndex, timestamps, context, true); + expect(values).toStrictEqual([100, 200, 1000]); + + convertDataToCubism(series, seriesIndex, timestamps, context, false); expect(values).toStrictEqual([100, 200, 1000]); }); - it('should return the same number of data points', () => { + it('should return less data points', () => { const input1 = { target: 'Field Name', datapoints: [ @@ -105,14 +154,14 @@ describe('convertDataToCubism', () => { }, }; - convertDataToCubism(series, seriesIndex, timestamps, context); + convertDataToCubism(series, seriesIndex, timestamps, context, true); expect(values).toStrictEqual([150, 400, 700, 1000]); }); }); describe('upSampleData', () => { it('should return a function', () => { - const result = upSampleData([1, 2, 3], [1000, 2000, 3000], 0); + const result = upSampleData([1, 2, 3], [1000, 2000, 3000]); expect(typeof result).toBe('function'); }); @@ -120,7 +169,7 @@ describe('upSampleData', () => { const dataPoints = [1, 2, 3]; const dataPointsTS = [1000, 2000, 3000]; const pointIndex = 0; - const fn = upSampleData(dataPoints, dataPointsTS, pointIndex); + const fn = upSampleData(dataPoints, dataPointsTS); const result = fn(1500, 0); expect(result).toBe(dataPoints[pointIndex]); }); @@ -129,7 +178,7 @@ describe('upSampleData', () => { const dataPoints = [1, 2, 3]; const dataPointsTS = [1000, 2000, 3000]; const pointIndex = 0; - const fn = upSampleData(dataPoints, dataPointsTS, pointIndex); + const fn = upSampleData(dataPoints, dataPointsTS); const result = fn(2000, 0); expect(result).toBe(dataPoints[pointIndex + 1]); }); @@ -137,21 +186,29 @@ describe('upSampleData', () => { it('should return the last point when there is no next point', () => { const dataPoints = [1, 2, 3]; const dataPointsTS = [1000, 2000, 3000]; - const pointIndex = 2; - const fn = upSampleData(dataPoints, dataPointsTS, pointIndex); - const result = fn(4000, 0); - expect(result).toBe(dataPoints[pointIndex]); + const fn = upSampleData(dataPoints, dataPointsTS); + fn(2000, 1); + fn(3000, 2); + let result = fn(4000, 3); + expect(result).toBe(3); }); it('should return a 4 elements array with the right values when called in map()', () => { const dataPoints = [1, 2, 3]; const dataPointsTS = [1000, 2000, 4000]; - const pointIndex = 0; const timestamps = [1000, 2000, 3000, 4000]; - let values = _.chain(timestamps).map(upSampleData(dataPoints, dataPointsTS, pointIndex)).value(); + let values = _.chain(timestamps).map(upSampleData(dataPoints, dataPointsTS)).value(); expect(values.length).toBe(4); expect(values).toStrictEqual([1, 2, 2, 3]); }); + it('should set values to 0 before and after', () => { + const dataPoints = [1, 2, 3]; + const dataPointsTS = [100, 110, 120]; + const timestamps = [70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200]; + let values = _.chain(timestamps).map(upSampleData(dataPoints, dataPointsTS)).value(); + expect(values.length).toBe(timestamps.length); + expect(values).toStrictEqual([1, 1, 1, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3]); + }); }); describe('testAverageValue', () => { @@ -324,4 +381,228 @@ describe('downSampleData', () => { expect(values.length).toBe(3); expect(values).toStrictEqual([10, 25, 40]); }); + it('should return the correct point when ts is less than nextPointTS', () => { + const dataPoints = [1, 2, 3]; + const dataPointsTS = [1000, 2000, 3000]; + let val: number[][] = []; + const override = { summaryType: 'sum' }; + + for (let i = 0; i < dataPoints.length; i++) { + val.push([dataPointsTS[i], dataPoints[i]]); + } + + let fn = downSampleData(dataPointsTS, val, override); + const result = fn(1000, 0); + expect(result).toBe(dataPoints[0]); + }); + + it('should return the next point when ts is greater than or equal to nextPointTS', () => { + const dataPoints = [1, 2, 3]; + const dataPointsTS = [1000, 2000, 3000]; + let val: number[][] = []; + const override = { summaryType: 'sum' }; + + for (let i = 0; i < dataPoints.length; i++) { + val.push([dataPointsTS[i], dataPoints[i]]); + } + + let fn = downSampleData(dataPointsTS, val, override); + const result = fn(2000, 1); + expect(result).toBe(dataPoints[1]); + }); + + it('should return the last point when there is no next point', () => { + const dataPoints = [1, 2, 3]; + const dataPointsTS = [1000, 2000, 3000]; + let val: number[][] = []; + const override = { summaryType: 'sum' }; + + for (let i = 0; i < dataPoints.length; i++) { + val.push([dataPointsTS[i], dataPoints[i]]); + } + + let fn = downSampleData([1000, 2000, 3000, 4000], val, override); + const result = fn(4000, 3); + expect(result).toBe(null); + }); + + it('should return a 5 elements array with the right values when called in map()', () => { + const dataPoints = [1, 2, 3, 4]; + const dataPointsTS = [1000, 2000, 5000]; + const timestamps = [1000, 2000, 3000, 4000, 5000]; + let val: number[][] = []; + const override = { summaryType: 'sum' }; + + for (let i = 0; i < dataPoints.length; i++) { + val.push([dataPointsTS[i], dataPoints[i]]); + } + + let values = _.chain(timestamps).map(downSampleData(timestamps, val, override)).value(); + expect(values.length).toBe(5); + expect(values).toStrictEqual([1, 2, 2, null, 3]); + }); + it('should set values to null before and after', () => { + const dataPoints = [1, 2, 3]; + const dataPointsTS = [100, 110, 120]; + const timestamps = [70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200]; + let val: number[][] = []; + const override = { summaryType: 'sum' }; + + for (let i = 0; i < dataPoints.length; i++) { + val.push([dataPointsTS[i], dataPoints[i]]); + } + + let values = _.chain(timestamps).map(downSampleData(timestamps, val, override)).value(); + expect(values.length).toBe(timestamps.length); + expect(values).toStrictEqual([null, null, null, 1, 2, 3, 3, null, null, null, null, null, null, null]); + }); + it('should not extend the last value after the end', () => { + const dataPoints = [1, 2, 3]; + const dataPointsTS = [100, 110, 120]; + const timestamps = [70, 80, 90, 100, 110, 120, 130]; + let val: number[][] = []; + const override = { summaryType: 'sum' }; + + for (let i = 0; i < dataPoints.length; i++) { + val.push([dataPointsTS[i], dataPoints[i]]); + } + + let values = _.chain(timestamps).map(downSampleData(timestamps, val, override)).value(); + expect(values.length).toBe(timestamps.length); + expect(values).toStrictEqual([null, null, null, 1, 2, 3, null]); + }); + it('should set values to 0 before and after add missing a bit even when not aligned', () => { + const dataPoints = [1, 2, 3]; + const dataPointsTS = [101, 111, 131]; + const timestamps = [70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200]; + let val: number[][] = []; + const override = { summaryType: 'sum' }; + + for (let i = 0; i < dataPoints.length; i++) { + val.push([dataPointsTS[i], dataPoints[i]]); + } + + let values = _.chain(timestamps).map(downSampleData(timestamps, val, override)).value(); + expect(values.length).toBe(timestamps.length); + expect(values).toStrictEqual([null, null, null, 1, 2, 2, 3, 3, null, null, null, null, null, null]); + }); + it('should not extend when the gap is too big', () => { + const dataPoints = [1, 2, 3, 4]; + const dataPointsTS = [101, 111, 172, 179]; + const timestamps = [70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200]; + let val: number[][] = []; + const override = { summaryType: 'avgerage' }; + + for (let i = 0; i < dataPoints.length; i++) { + val.push([dataPointsTS[i], dataPoints[i]]); + } + + let values = _.chain(timestamps).map(downSampleData(timestamps, val, override)).value(); + expect(values.length).toBe(timestamps.length); + expect(values).toStrictEqual([null, null, null, 1, 2, 2, null, null, null, null, 3.5, 3.5, null, null]); + }); + it('should convertAllDataToCubism just work', () => { + const input1 = { + target: 'Field Name', + datapoints: [ + [100, 1], + [200, 2], + [300, 3], + ], + }; + const input2 = { + target: 'Field Name', + datapoints: [ + [100, 1], + [200, 2], + [300, 3], + [300, 4], + ], + }; + let series = [toDataFrame(input1), toDataFrame(input2)]; + const timestamps = [1, 2, 3, 4]; + const context = { + metric: (callback: any, name: string) => {}, + }; + + expect(() => convertAllDataToCubism(series, timestamps, context, 1)).not.toThrow(); + }); + it('should convertAllDataToCubism just work even without a Time field', () => { + const input1 = { + target: 'Field Name', + datapoints: [ + [100, 1], + [200, 2], + [300, 3], + ], + }; + let serie = toDataFrame(input1); + serie.fields[0].name = 'foo'; + let series = [serie]; + const timestamps = [1, 2, 3, 4]; + let values; + const context = { + metric: (callback: any, name: string) => { + callback(1, 20, 1, (a: any, b: any) => { + values = b; + }); + }, + }; + + expect(() => convertAllDataToCubism(series, timestamps, context, 1)).not.toThrow(); + expect(values).toStrictEqual([100, 200, 300, 300]); + }); + it('should convertAllDataToCubism not add points if there is too much blanks', () => { + const input1 = { + target: 'Field Name', + datapoints: [ + [100, 1], + [100, 2], + [200, 5], + [300, 10], + ], + }; + let serie = toDataFrame(input1); + serie.fields[0].name = 'foo'; + let series = [serie]; + const timestamps = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let values; + const context = { + metric: (callback: any, name: string) => { + callback(1, 20, 1, (a: any, b: any) => { + values = b; + }); + }, + }; + + expect(() => convertAllDataToCubism(series, timestamps, context, 1)).not.toThrow(); + expect(values).toStrictEqual([100, 100, 100, null, 200, 200, null, null, null, 300]); + }); + + it('should convertAllDataToCubism with a larger timeserie', () => { + const input1 = { + target: 'Field Name', + datapoints: [ + [100, 1], + [200, 2], + [300, 3], + [400, 4], + [500, 5], + ], + }; + let serie = toDataFrame(input1); + let series = [serie]; + const timestamps = [1, 3, 5]; + let values; + const context = { + metric: (callback: any, name: string) => { + callback(1, 20, 1, (a: any, b: any) => { + values = b; + }); + }, + }; + + expect(() => convertAllDataToCubism(series, timestamps, context, 1)).not.toThrow(); + expect(values).toStrictEqual([150, 350, 500]); + }); }); diff --git a/src/tests/misc_utils.test.ts b/src/tests/misc_utils.test.ts new file mode 100644 index 0000000..adec143 --- /dev/null +++ b/src/tests/misc_utils.test.ts @@ -0,0 +1,23 @@ +// convertDataToCubism +import { enableDebug, log_debug } from '../misc_utils'; + +describe('misc utils', () => { + it('should not log when debug is not enabled', () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + log_debug('let met log something'); + expect(consoleSpy).not.toHaveBeenCalled(); + }); + it('should log when debug is not enabled', () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + enableDebug(); + log_debug('let met log something'); + expect(consoleSpy).toHaveBeenCalledTimes(1); + }); + it('should log when debug is not enabled even with parameters', () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + enableDebug(); + let v = 1; + log_debug('let met log something', v); + expect(consoleSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/webpack.config.ts b/webpack.config.ts index 1c49c7f..7f6b594 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -1,28 +1,22 @@ import type { Configuration } from 'webpack'; -import { mergeWithRules } from 'webpack-merge'; +import { merge } from 'webpack-merge'; import grafanaConfig from './.config/webpack/webpack.config'; const config = async (env: any): Promise => { const baseConfig = await grafanaConfig(env); - const customConfig = { + const customConfig: Configuration = { module: { rules: [ { test: /\.js$/, - enforce: "pre", - use: ["source-map-loader"], - } - ] + enforce: 'pre', + use: ['source-map-loader'], + }, + ], }, - mode: 'development' - } - return mergeWithRules({ - module: { - rules: { - exclude: 'replace', - }, - }, - })(baseConfig, customConfig); + mode: 'development', + }; + return merge(baseConfig, customConfig); }; export default config;