diff --git a/devenv/dev-dashboards/transforms/extract-json-paths.json b/devenv/dev-dashboards/transforms/extract-json-paths.json new file mode 100644 index 00000000000..d709a444680 --- /dev/null +++ b/devenv/dev-dashboards/transforms/extract-json-paths.json @@ -0,0 +1,316 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "gdev", + "transform", + "transformations", + "extract", + "json" + ], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 8, + "links": [], + "liveNow": false, + "panels": [ + { + "id": 2, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "type": "table", + "title": "Extracting individual values", + "transformations": [ + { + "id": "extractFields", + "options": { + "format": "json", + "jsonPaths": [ + { + "alias": "Temperature", + "path": "[8].testdata.source1.value1" + }, + { + "alias": "Primes", + "path": "[8].testdata.source2[1][3]" + } + ], + "keepTime": true, + "replace": true, + "source": "Value" + } + } + ], + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "pluginVersion": "9.4.0-pre", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "footer": { + "show": false, + "reducer": [ + "sum" + ], + "countRows": false, + "fields": "" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "rawFrameContent": "[{\"schema\":{\"refId\":\"A\",\"meta\":{\"channel\":\"ds/bHGPS1h4z/1s/test\",\"transformations\":[\"extractFields\",\"extractFields\",\"extractFields\"]},\"fields\":[{\"name\":\"Time\",\"type\":\"time\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}},{\"name\":\"Value\",\"type\":\"other\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"mappings\":[],\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}}]},\"data\":{\"values\":[[1673543683471,1673543689063,1673543695050],[[\"2023-01-12T17:14:44.419Z\",62,141,79,29,79,-29,29,{\"testdata\":{\"source1\":{\"value1\":9,\"value2\":18},\"source2\":[[0,1,2,3,4,5,6,7,8,9],[7,11,13,17,19,23,27,29]]}}],[\"2023-01-12T17:14:50.050Z\",62,143,81,29,81,-29,29,{\"testdata\":{\"source1\":{\"value1\":10,\"value2\":20},\"source2\":[[1,2,3,4,5,6,7,8,9,10],[11,13,17,19,23,27,29,31]]}}],[\"2023-01-12T17:14:55.050Z\",61,146,80,22,85,-28,28,{\"testdata\":{\"source1\":{\"value1\":11,\"value2\":22},\"source2\":[[3,4,5,6,7,8,9,10,11,12],[13,17,19,23,27,29,31,37,41]]}}]]]}}]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "description": "Some data sources (for example MQTT) might be consuming incomparable metrics packaged in the same JSON payload. We can use this extract fields transformation's JSON option to select the specific fields we want, and alias the values to help classify unlabeled or unstructured data." + }, + { + "id": 3, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 0 + }, + "type": "timeseries", + "title": "Visualizing extracted JSON", + "transformations": [ + { + "id": "extractFields", + "options": { + "format": "json", + "jsonPaths": [ + { + "alias": "Temperature", + "path": "[8].testdata.source1.value1" + } + ], + "keepTime": true, + "replace": true, + "source": "Value" + } + } + ], + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "pluginVersion": "9.4.0-pre", + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineInterpolation": "linear", + "barAlignment": 0, + "lineWidth": 1, + "fillOpacity": 0, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "auto", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisPlacement": "auto", + "axisLabel": "", + "axisColorMode": "text", + "scaleDistribution": { + "type": "linear" + }, + "axisCenteredZero": false, + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "options": { + "tooltip": { + "mode": "single", + "sort": "none" + }, + "legend": { + "showLegend": true, + "displayMode": "list", + "placement": "bottom", + "calcs": [] + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "rawFrameContent": "[{\"schema\":{\"refId\":\"A\",\"meta\":{\"channel\":\"ds/bHGPS1h4z/1s/test\",\"transformations\":[\"extractFields\",\"extractFields\",\"extractFields\"]},\"fields\":[{\"name\":\"Time\",\"type\":\"time\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}},{\"name\":\"Value\",\"type\":\"other\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"mappings\":[],\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}}]},\"data\":{\"values\":[[1673543683471,1673543689063,1673543695050],[[\"2023-01-12T17:14:44.419Z\",62,141,79,29,79,-29,29,{\"testdata\":{\"source1\":{\"value1\":9,\"value2\":18},\"source2\":[[0,1,2,3,4,5,6,7,8,9],[7,11,13,17,19,23,27,29]]}}],[\"2023-01-12T17:14:50.050Z\",62,143,81,29,81,-29,29,{\"testdata\":{\"source1\":{\"value1\":10,\"value2\":20},\"source2\":[[1,2,3,4,5,6,7,8,9,10],[11,13,17,19,23,27,29,31]]}}],[\"2023-01-12T17:14:55.050Z\",61,146,80,22,85,-28,28,{\"testdata\":{\"source1\":{\"value1\":11,\"value2\":22},\"source2\":[[3,4,5,6,7,8,9,10,11,12],[13,17,19,23,27,29,31,37,41]]}}]]]}}]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "description": "" + }, + { + "id": 4, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 9 + }, + "type": "table", + "title": "Raw data", + "transformations": [], + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "pluginVersion": "9.4.0-pre", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "color": { + "mode": "thresholds" + } + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "footer": { + "show": false, + "reducer": [ + "sum" + ], + "countRows": false, + "fields": "" + } + }, + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "gdev-testdata" + }, + "rawFrameContent": "[{\"schema\":{\"refId\":\"A\",\"meta\":{\"channel\":\"ds/bHGPS1h4z/1s/test\",\"transformations\":[\"extractFields\",\"extractFields\",\"extractFields\"]},\"fields\":[{\"name\":\"Time\",\"type\":\"time\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}},{\"name\":\"Value\",\"type\":\"other\",\"config\":{\"custom\":{\"align\":\"auto\",\"displayMode\":\"auto\",\"inspect\":false},\"color\":{\"mode\":\"thresholds\"},\"mappings\":[],\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]}}}]},\"data\":{\"values\":[[1673543683471,1673543689063,1673543695050],[[\"2023-01-12T17:14:44.419Z\",62,141,79,29,79,-29,29,{\"testdata\":{\"source1\":{\"value1\":9,\"value2\":18},\"source2\":[[0,1,2,3,4,5,6,7,8,9],[7,11,13,17,19,23,27,29]]}}],[\"2023-01-12T17:14:50.050Z\",62,143,81,29,81,-29,29,{\"testdata\":{\"source1\":{\"value1\":10,\"value2\":20},\"source2\":[[1,2,3,4,5,6,7,8,9,10],[11,13,17,19,23,27,29,31]]}}],[\"2023-01-12T17:14:55.050Z\",61,146,80,22,85,-28,28,{\"testdata\":{\"source1\":{\"value1\":11,\"value2\":22},\"source2\":[[3,4,5,6,7,8,9,10,11,12],[13,17,19,23,27,29,31,37,41]]}}]]]}}]", + "refId": "A", + "scenarioId": "raw_frame" + } + ], + "description": "" + } + ], + "revision": 1, + "schemaVersion": 37, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "2023-01-12T17:14:42.652Z", + "to": "2023-01-12T17:14:55.358Z" + }, + "timepicker": {}, + "timezone": "", + "title": "Test extractFields JSON", + "uid": "pD4vPYhVz", + "version": 3, + "weekStart": "" +} diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index db37c3de6e6..60a5faa1694 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -177,6 +177,13 @@ local dashboard = grafana.dashboard; id: 0, } }, + dashboard.new('extract-json-paths', import '../dev-dashboards/transforms/extract-json-paths.json') + + resource.addMetadata('folder', 'dev-dashboards') + + { + spec+: { + id: 0, + } + }, dashboard.new('gauge-multi-series', import '../dev-dashboards/panel-gauge/gauge-multi-series.json') + resource.addMetadata('folder', 'dev-dashboards') + { diff --git a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx index 6d89b83b6fe..723893e81a1 100644 --- a/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/extractFields/ExtractFieldsTransformerEditor.tsx @@ -2,17 +2,19 @@ import React from 'react'; import { DataTransformerID, + TransformerRegistryItem, + TransformerUIProps, FieldNamePickerConfigSettings, SelectableValue, StandardEditorsRegistryItem, - TransformerRegistryItem, - TransformerUIProps, } from '@grafana/data'; -import { InlineField, InlineFieldRow, InlineSwitch, Select } from '@grafana/ui'; +import { InlineField, InlineFieldRow, Select, InlineSwitch } from '@grafana/ui'; import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker'; -import { ExtractFieldsOptions, extractFieldsTransformer } from './extractFields'; -import { FieldExtractorID, fieldExtractors } from './fieldExtractors'; +import { JSONPathEditor } from './components/JSONPathEditor'; +import { extractFieldsTransformer } from './extractFields'; +import { fieldExtractors } from './fieldExtractors'; +import { ExtractFieldsOptions, FieldExtractorID, JSONPath } from './types'; const fieldNamePickerSettings: StandardEditorsRegistryItem = { settings: { @@ -43,13 +45,31 @@ export const extractFieldsTransformerEditor: React.FC { + onChange({ + ...options, + jsonPaths, + }); + }; + const onToggleReplace = () => { + if (options.replace) { + options.keepTime = false; + } + onChange({ ...options, replace: !options.replace, }); }; + const onToggleKeepTime = () => { + onChange({ + ...options, + keepTime: !options.keepTime, + }); + }; + const format = fieldExtractors.selectOptions(options.format ? [options.format] : undefined); return ( @@ -75,11 +95,19 @@ export const extractFieldsTransformerEditor: React.FC + {options.format === 'json' && } + {options.replace && ( + + + + + + )} ); }; diff --git a/public/app/features/transformers/extractFields/components/JSONPathEditor.tsx b/public/app/features/transformers/extractFields/components/JSONPathEditor.tsx new file mode 100644 index 00000000000..a48e954bea4 --- /dev/null +++ b/public/app/features/transformers/extractFields/components/JSONPathEditor.tsx @@ -0,0 +1,130 @@ +import { css, cx } from '@emotion/css'; +import React, { useState } from 'react'; + +import { Button, InlineField, InlineFieldRow, IconButton, Input } from '@grafana/ui'; + +import { JSONPath } from '../types'; + +interface Props { + options: JSONPath[]; + onChange: (options: JSONPath[]) => void; +} + +export function JSONPathEditor({ options, onChange }: Props) { + const [paths, setPaths] = useState(options); + const tooltips = getTooltips(); + const style = getStyle(); + + const addJSONPath = () => { + paths.push({ path: '' }); + setPaths([...paths]); + onBlur(); + }; + + const removeJSONPath = (keyPath: number) => { + if (paths) { + paths.splice(keyPath, 1); + setPaths([...paths]); + onBlur(); + } + }; + + const onJSONPathChange = (event: React.SyntheticEvent, keyPath: number, type: 'alias' | 'path') => { + if (paths) { + if (type === 'alias') { + paths[keyPath].alias = event.currentTarget.value ?? ''; + } else { + paths[keyPath].path = event.currentTarget.value ?? ''; + } + setPaths([...paths]); + } + }; + + const onBlur = () => { + onChange(paths); + }; + + return ( +
    + {paths && + paths.map((path: JSONPath, key: number) => ( +
  1. + + + ) => onJSONPathChange(event, key, 'path')} + value={path.path} + placeholder='A valid json path, e.g. "object.value1" or "object.value2[0]"' + /> + + + ) => onJSONPathChange(event, key, 'alias')} + /> + + + removeJSONPath(key)} name={'trash-alt'} /> + + +
  2. + ))} + + + +
+ ); +} + +const getTooltips = () => { + const mapValidPaths = [ + { path: 'object', description: '=> extract fields from object' }, + { path: 'object.value1', description: '=> extract value1' }, + { path: 'object.value2', description: '=> extract value2' }, + { path: 'object.value2[0]', description: '=> extract value2 first element' }, + { path: 'object.value2[1]', description: '=> extract value2 second element' }, + ]; + + return { + field: ( +
+ A valid path of an json object. +
+ JSON Value: +
+
+          
+            {['{', '  "object": {', '    "value1": "hello world"', '    "value2": [1, 2, 3, 4]', '  }', '}'].join('\n')}
+          
+        
+ Valid Paths: + {mapValidPaths.map((value, key) => { + return ( +

+ {value.path} + {value.description} +

+ ); + })} +
+ ), + alias: 'An alias name for the variable in the dashboard. If left blank the given path will be used.', + }; +}; + +function getStyle() { + return { + list: css` + margin-left: 20px; + `, + removeIcon: css` + margin: 0 0 0 4px; + align-items: center; + `, + }; +} diff --git a/public/app/features/transformers/extractFields/extractFields.test.ts b/public/app/features/transformers/extractFields/extractFields.test.ts index bc810be25b6..79898512beb 100644 --- a/public/app/features/transformers/extractFields/extractFields.test.ts +++ b/public/app/features/transformers/extractFields/extractFields.test.ts @@ -1,6 +1,8 @@ +import { ArrayVector, DataFrame, Field, FieldType } from '@grafana/data'; import { toDataFrame } from '@grafana/data/src/dataframe/processDataFrame'; -import { ExtractFieldsOptions, extractFieldsTransformer } from './extractFields'; +import { extractFieldsTransformer } from './extractFields'; +import { ExtractFieldsOptions, FieldExtractorID } from './types'; describe('Fields from JSON', () => { it('adds fields from JSON in string', async () => { @@ -40,8 +42,186 @@ describe('Fields from JSON', () => { } `); }); + + it('Get nested path values', () => { + const cfg: ExtractFieldsOptions = { + replace: true, + source: 'JSON', + format: FieldExtractorID.JSON, + jsonPaths: [ + { path: 'object.nestedArray[0]' }, + { path: 'object.nestedArray[1]' }, + { path: 'object.nestedString' }, + ], + }; + const ctx = { interpolate: (v: string) => v }; + + const frames = extractFieldsTransformer.transformer(cfg, ctx)([testDataFrame]); + expect(frames.length).toEqual(1); + expect(frames[0]).toMatchInlineSnapshot(` + { + "fields": [ + { + "config": {}, + "name": "object.nestedArray[0]", + "type": "number", + "values": [ + 1, + ], + }, + { + "config": {}, + "name": "object.nestedArray[1]", + "type": "number", + "values": [ + 2, + ], + }, + { + "config": {}, + "name": "object.nestedString", + "type": "string", + "values": [ + "Hallo World", + ], + }, + ], + "length": 1, + } + `); + }); + + it('Keep time field on replace', () => { + const cfg: ExtractFieldsOptions = { + replace: true, + keepTime: true, + source: 'JSON', + format: FieldExtractorID.JSON, + jsonPaths: [ + { path: 'object.nestedArray[2]' }, + { path: 'object.nestedArray[3]' }, + { path: 'object.nestedString' }, + ], + }; + const ctx = { interpolate: (v: string) => v }; + + const frames = extractFieldsTransformer.transformer(cfg, ctx)([testDataFrame]); + expect(frames.length).toEqual(1); + expect(frames[0]).toMatchInlineSnapshot(` + { + "fields": [ + { + "config": {}, + "name": "Time", + "state": { + "displayName": "Time", + "multipleFrames": false, + }, + "type": "time", + "values": [ + 1669638911691, + ], + }, + { + "config": {}, + "name": "object.nestedArray[2]", + "type": "number", + "values": [ + 3, + ], + }, + { + "config": {}, + "name": "object.nestedArray[3]", + "type": "number", + "values": [ + 4, + ], + }, + { + "config": {}, + "name": "object.nestedString", + "type": "string", + "values": [ + "Hallo World", + ], + }, + ], + "length": 1, + } + `); + }); + + it('Path is invalid', () => { + const cfg: ExtractFieldsOptions = { + replace: true, + source: 'JSON', + format: FieldExtractorID.JSON, + jsonPaths: [{ path: 'object.nestedString' }, { path: 'invalid.path' }], + }; + const ctx = { interpolate: (v: string) => v }; + + const frames = extractFieldsTransformer.transformer(cfg, ctx)([testDataFrame]); + expect(frames.length).toEqual(1); + expect(frames[0]).toMatchInlineSnapshot(` + { + "fields": [ + { + "config": {}, + "name": "object.nestedString", + "type": "string", + "values": [ + "Hallo World", + ], + }, + { + "config": {}, + "name": "invalid.path", + "type": "string", + "values": [ + "Not Found", + ], + }, + ], + "length": 1, + } + `); + }); }); +const testFieldTime: Field = { + config: {}, + name: 'Time', + type: FieldType.time, + values: new ArrayVector([1669638911691]), +}; + +const testFieldString: Field = { + config: {}, + name: 'String', + type: FieldType.string, + values: new ArrayVector(['Hallo World']), +}; + +const testFieldJSON: Field = { + config: {}, + name: 'JSON', + type: FieldType.string, + values: new ArrayVector([ + JSON.stringify({ + object: { + nestedArray: [1, 2, 3, 4], + nestedString: 'Hallo World', + }, + }), + ]), +}; + +const testDataFrame: DataFrame = { + fields: [testFieldTime, testFieldString, testFieldJSON], + length: 1, +}; + const appl = [ [ '1636678740000000000', diff --git a/public/app/features/transformers/extractFields/extractFields.ts b/public/app/features/transformers/extractFields/extractFields.ts index 8aef7c62dee..b807de1aee8 100644 --- a/public/app/features/transformers/extractFields/extractFields.ts +++ b/public/app/features/transformers/extractFields/extractFields.ts @@ -1,4 +1,4 @@ -import { isString } from 'lodash'; +import { isString, get } from 'lodash'; import { map } from 'rxjs/operators'; import { @@ -12,13 +12,8 @@ import { } from '@grafana/data'; import { findField } from 'app/features/dimensions'; -import { FieldExtractorID, fieldExtractors } from './fieldExtractors'; - -export interface ExtractFieldsOptions { - source?: string; - format?: FieldExtractorID; - replace?: boolean; -} +import { fieldExtractors } from './fieldExtractors'; +import { ExtractFieldsOptions, FieldExtractorID, JSONPath } from './types'; export const extractFieldsTransformer: SynchronousDataTransformerInfo = { id: DataTransformerID.extractFields, @@ -40,7 +35,9 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da if (!options.source) { return frame; } + const source = findField(frame, options.source); + if (!source) { // this case can happen when there are multiple queries return frame; @@ -57,6 +54,7 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da for (let i = 0; i < count; i++) { let obj = source.values.get(i); + if (isString(obj)) { try { obj = ext.parse(obj); @@ -64,6 +62,22 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da obj = {}; // empty } } + + if (options.format === FieldExtractorID.JSON && options.jsonPaths && options.jsonPaths?.length > 0) { + const newObj: { [k: string]: unknown } = {}; + // filter out empty paths + const filteredPaths = options.jsonPaths.filter((path: JSONPath) => path.path); + + if (filteredPaths.length > 0) { + filteredPaths.forEach((path: JSONPath) => { + const key = path.alias && path.alias.length > 0 ? path.alias : path.path; + newObj[key] = get(obj, path.path) ?? 'Not Found'; + }); + + obj = newObj; + } + } + for (const [key, val] of Object.entries(obj)) { let buffer = values.get(key); if (buffer == null) { @@ -85,9 +99,17 @@ function addExtractedFields(frame: DataFrame, options: ExtractFieldsOptions): Da } as Field; }); + if (options.keepTime) { + const sourceTime = findField(frame, 'Time') || findField(frame, 'time'); + if (sourceTime) { + fields.unshift(sourceTime); + } + } + if (!options.replace) { fields.unshift(...frame.fields); } + return { ...frame, fields, diff --git a/public/app/features/transformers/extractFields/fieldExtractor.test.ts b/public/app/features/transformers/extractFields/fieldExtractor.test.ts index 8e5f9865a31..8cb58383877 100644 --- a/public/app/features/transformers/extractFields/fieldExtractor.test.ts +++ b/public/app/features/transformers/extractFields/fieldExtractor.test.ts @@ -1,4 +1,5 @@ -import { fieldExtractors, FieldExtractorID } from './fieldExtractors'; +import { fieldExtractors } from './fieldExtractors'; +import { FieldExtractorID } from './types'; describe('Extract fields from text', () => { it('JSON extractor', async () => { diff --git a/public/app/features/transformers/extractFields/fieldExtractors.ts b/public/app/features/transformers/extractFields/fieldExtractors.ts index 45b91aecf4f..ba8af2b78d6 100644 --- a/public/app/features/transformers/extractFields/fieldExtractors.ts +++ b/public/app/features/transformers/extractFields/fieldExtractors.ts @@ -1,10 +1,6 @@ import { Registry, RegistryItem } from '@grafana/data'; -export enum FieldExtractorID { - JSON = 'json', - KeyValues = 'kvp', - Auto = 'auto', -} +import { FieldExtractorID } from './types'; export interface FieldExtractor extends RegistryItem { parse: (v: string) => Record | undefined; diff --git a/public/app/features/transformers/extractFields/types.ts b/public/app/features/transformers/extractFields/types.ts new file mode 100644 index 00000000000..e0f63aab4d1 --- /dev/null +++ b/public/app/features/transformers/extractFields/types.ts @@ -0,0 +1,17 @@ +export enum FieldExtractorID { + JSON = 'json', + KeyValues = 'kvp', + Auto = 'auto', +} + +export interface JSONPath { + path: string; + alias?: string; +} +export interface ExtractFieldsOptions { + source?: string; + jsonPaths?: JSONPath[]; + format?: FieldExtractorID; + replace?: boolean; + keepTime?: boolean; +}