Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
N
nexpie-grafana-theme
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Registry
Registry
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kornkitt Poolsup
nexpie-grafana-theme
Commits
f8e0adb1
Unverified
Commit
f8e0adb1
authored
Oct 14, 2020
by
Ryan McKinley
Committed by
GitHub
Oct 14, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Transformations: improve the reduce transformer (#27875)
parent
db071e49
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
265 additions
and
99 deletions
+265
-99
packages/grafana-data/src/transformations/matchers/predicates.ts
+5
-1
packages/grafana-data/src/transformations/transformers/reduce.test.ts
+30
-1
packages/grafana-data/src/transformations/transformers/reduce.ts
+147
-76
packages/grafana-e2e-selectors/src/selectors/components.ts
+1
-0
public/app/core/components/TransformersUI/ReduceTransformerEditor.tsx
+82
-21
No files found.
packages/grafana-data/src/transformations/matchers/predicates.ts
View file @
f8e0adb1
import
{
Field
,
DataFrame
}
from
'../../types/dataFrame'
;
import
{
Field
,
DataFrame
,
FieldType
}
from
'../../types/dataFrame'
;
import
{
MatcherID
}
from
'./ids'
;
import
{
getFieldMatcher
,
fieldMatchers
,
getFrameMatchers
,
frameMatchers
}
from
'../matchers'
;
import
{
FieldMatcherInfo
,
MatcherConfig
,
FrameMatcherInfo
}
from
'../../types/transformations'
;
...
...
@@ -191,6 +191,10 @@ export const neverFieldMatcher = (field: Field) => {
return
false
;
};
export
const
notTimeFieldMatcher
=
(
field
:
Field
)
=>
{
return
field
.
type
!==
FieldType
.
time
;
};
export
const
neverFrameMatcher
=
(
frame
:
DataFrame
)
=>
{
return
false
;
};
...
...
packages/grafana-data/src/transformations/transformers/reduce.test.ts
View file @
f8e0adb1
...
...
@@ -2,11 +2,13 @@ import { ReducerID } from '../fieldReducer';
import
{
DataTransformerID
}
from
'./ids'
;
import
{
toDataFrame
}
from
'../../dataframe/processDataFrame'
;
import
{
mockTransformationsRegistry
}
from
'../../utils/tests/mockTransformationsRegistry'
;
import
{
reduceTransformer
}
from
'./reduce'
;
import
{
reduce
Fields
,
reduce
Transformer
}
from
'./reduce'
;
import
{
transformDataFrame
}
from
'../transformDataFrame'
;
import
{
Field
,
FieldType
}
from
'../../types'
;
import
{
ArrayVector
}
from
'../../vector'
;
import
{
observableTester
}
from
'../../utils/tests/observableTester'
;
import
{
notTimeFieldMatcher
}
from
'../matchers/predicates'
;
import
{
DataFrameView
}
from
'../../dataframe'
;
const
seriesAWithSingleField
=
toDataFrame
({
name
:
'A'
,
...
...
@@ -254,4 +256,31 @@ describe('Reducer Transformer', () => {
done
,
});
});
it
(
'reduces fields with single calculator'
,
()
=>
{
const
frames
=
reduceFields
(
[
seriesAWithSingleField
,
seriesAWithMultipleFields
],
// data
notTimeFieldMatcher
,
// skip time fields
[
ReducerID
.
last
]
// only one
);
// Convert each frame to a structure with the same fields
expect
(
frames
.
length
).
toEqual
(
2
);
expect
(
frames
[
0
].
length
).
toEqual
(
1
);
expect
(
frames
[
1
].
length
).
toEqual
(
1
);
const
view0
=
new
DataFrameView
<
any
>
(
frames
[
0
]);
const
view1
=
new
DataFrameView
<
any
>
(
frames
[
1
]);
expect
({
...
view0
.
get
(
0
)
}).
toMatchInlineSnapshot
(
`
Object {
"temperature": 6,
}
`
);
expect
({
...
view1
.
get
(
0
)
}).
toMatchInlineSnapshot
(
`
Object {
"humidity": 10000.6,
"temperature": 6,
}
`
);
});
});
packages/grafana-data/src/transformations/transformers/reduce.ts
View file @
f8e0adb1
...
...
@@ -3,19 +3,24 @@ import { map } from 'rxjs/operators';
import
{
DataTransformerID
}
from
'./ids'
;
import
{
DataTransformerInfo
,
MatcherConfig
}
from
'../../types/transformations'
;
import
{
fieldReducers
,
reduceField
,
ReducerID
}
from
'../fieldReducer'
;
import
{
alwaysFieldMatcher
}
from
'../matchers/predicates'
;
import
{
alwaysFieldMatcher
,
notTimeFieldMatcher
}
from
'../matchers/predicates'
;
import
{
DataFrame
,
Field
,
FieldType
}
from
'../../types/dataFrame'
;
import
{
ArrayVector
}
from
'../../vector/ArrayVector'
;
import
{
KeyValue
}
from
'../../types/data'
;
import
{
guessFieldTypeForField
}
from
'../../dataframe/processDataFrame'
;
import
{
getFieldMatcher
}
from
'../matchers'
;
import
{
FieldMatcherID
}
from
'../matchers/ids'
;
import
{
filterFieldsTransformer
}
from
'./filter'
;
import
{
getFieldDisplayName
}
from
'../../field'
;
import
{
FieldMatcher
}
from
'../../types/transformations'
;
export
enum
ReduceTransformerMode
{
SeriesToRows
=
'seriesToRows'
,
// default
ReduceFields
=
'reduceFields'
,
// same structure, add additional row for each type
}
export
interface
ReduceTransformerOptions
{
reducers
:
ReducerID
[];
fields
?:
MatcherConfig
;
// Assume all fields
mode
?:
ReduceTransformerMode
;
includeTimeField
?:
boolean
;
}
export
const
reduceTransformer
:
DataTransformerInfo
<
ReduceTransformerOptions
>
=
{
...
...
@@ -33,89 +38,111 @@ export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> =
operator
:
options
=>
source
=>
source
.
pipe
(
map
(
data
=>
{
const
matcher
=
options
.
fields
?
getFieldMatcher
(
options
.
fields
)
:
alwaysFieldMatcher
;
const
calculators
=
options
.
reducers
&&
options
.
reducers
.
length
?
fieldReducers
.
list
(
options
.
reducers
)
:
[];
const
reducers
=
calculators
.
map
(
c
=>
c
.
id
);
const
processed
:
DataFrame
[]
=
[];
for
(
let
seriesIndex
=
0
;
seriesIndex
<
data
.
length
;
seriesIndex
++
)
{
const
series
=
data
[
seriesIndex
];
const
values
:
ArrayVector
[]
=
[];
const
fields
:
Field
[]
=
[];
const
byId
:
KeyValue
<
ArrayVector
>
=
{};
values
.
push
(
new
ArrayVector
());
// The name
fields
.
push
({
name
:
'Field'
,
type
:
FieldType
.
string
,
values
:
values
[
0
],
config
:
{},
});
for
(
const
info
of
calculators
)
{
const
vals
=
new
ArrayVector
();
byId
[
info
.
id
]
=
vals
;
values
.
push
(
vals
);
fields
.
push
({
name
:
info
.
name
,
type
:
FieldType
.
other
,
// UNKNOWN until after we call the functions
values
:
values
[
values
.
length
-
1
],
config
:
{},
});
}
if
(
!
options
?.
reducers
?.
length
)
{
return
data
;
// nothing selected
}
for
(
let
i
=
0
;
i
<
series
.
fields
.
length
;
i
++
)
{
const
field
=
series
.
fields
[
i
];
const
matcher
=
options
.
fields
?
getFieldMatcher
(
options
.
fields
)
:
options
.
includeTimeField
&&
options
.
mode
===
ReduceTransformerMode
.
ReduceFields
?
alwaysFieldMatcher
:
notTimeFieldMatcher
;
if
(
field
.
type
===
FieldType
.
time
)
{
continue
;
}
// Collapse all matching fields into a single row
if
(
options
.
mode
===
ReduceTransformerMode
.
ReduceFields
)
{
return
reduceFields
(
data
,
matcher
,
options
.
reducers
);
}
if
(
matcher
(
field
,
series
,
data
))
{
const
results
=
reduceField
({
field
,
reducers
,
});
// Add a row for each series
const
res
=
reduceSeriesToRows
(
data
,
matcher
,
options
.
reducers
);
return
res
?
[
res
]
:
[];
})
),
};
// Update the name list
const
fieldName
=
getFieldDisplayName
(
field
,
series
,
data
);
/**
* @internal only exported for testing
*/
export
function
reduceSeriesToRows
(
data
:
DataFrame
[],
matcher
:
FieldMatcher
,
reducerId
:
ReducerID
[]
):
DataFrame
|
undefined
{
const
calculators
=
fieldReducers
.
list
(
reducerId
);
const
reducers
=
calculators
.
map
(
c
=>
c
.
id
);
const
processed
:
DataFrame
[]
=
[];
for
(
const
series
of
data
)
{
const
values
:
ArrayVector
[]
=
[];
const
fields
:
Field
[]
=
[];
const
byId
:
KeyValue
<
ArrayVector
>
=
{};
values
.
push
(
new
ArrayVector
());
// The name
fields
.
push
({
name
:
'Field'
,
type
:
FieldType
.
string
,
values
:
values
[
0
],
config
:
{},
});
for
(
const
info
of
calculators
)
{
const
vals
=
new
ArrayVector
();
byId
[
info
.
id
]
=
vals
;
values
.
push
(
vals
);
fields
.
push
({
name
:
info
.
name
,
type
:
FieldType
.
other
,
// UNKNOWN until after we call the functions
values
:
values
[
values
.
length
-
1
],
config
:
{},
});
}
values
[
0
].
buffer
.
push
(
fieldName
);
for
(
let
i
=
0
;
i
<
series
.
fields
.
length
;
i
++
)
{
const
field
=
series
.
fields
[
i
];
for
(
const
info
of
calculators
)
{
const
v
=
results
[
info
.
id
];
byId
[
info
.
id
].
buffer
.
push
(
v
);
}
}
}
if
(
matcher
(
field
,
series
,
data
))
{
const
results
=
reduceField
({
field
,
reducers
,
});
for
(
const
f
of
fields
)
{
const
t
=
guessFieldTypeForField
(
f
);
// Update the name list
const
fieldName
=
getFieldDisplayName
(
field
,
series
,
data
);
if
(
t
)
{
f
.
type
=
t
;
}
}
values
[
0
].
buffer
.
push
(
fieldName
);
processed
.
push
({
...
series
,
// Same properties, different fields
fields
,
length
:
values
[
0
].
length
,
});
for
(
const
info
of
calculators
)
{
const
v
=
results
[
info
.
id
];
byId
[
info
.
id
].
buffer
.
push
(
v
);
}
}
}
return
processed
;
}),
filterFieldsTransformer
.
operator
({
exclude
:
{
id
:
FieldMatcherID
.
time
}
}),
map
(
mergeResults
)
),
};
for
(
const
f
of
fields
)
{
const
t
=
guessFieldTypeForField
(
f
);
if
(
t
)
{
f
.
type
=
t
;
}
}
const
mergeResults
=
(
data
:
DataFrame
[])
=>
{
if
(
data
.
length
<=
1
)
{
return
data
;
processed
.
push
({
...
series
,
// Same properties, different fields
fields
,
length
:
values
[
0
].
length
,
});
}
return
mergeResults
(
processed
);
}
/**
* @internal only exported for testing
*/
export
function
mergeResults
(
data
:
DataFrame
[]):
DataFrame
|
undefined
{
if
(
!
data
?.
length
)
{
return
undefined
;
}
const
baseFrame
=
data
[
0
];
...
...
@@ -138,6 +165,50 @@ const mergeResults = (data: DataFrame[]) => {
baseFrame
.
name
=
undefined
;
baseFrame
.
length
=
baseFrame
.
fields
[
0
].
values
.
length
;
return
baseFrame
;
}
return
[
baseFrame
];
};
/**
* @internal -- only exported for testing
*/
export
function
reduceFields
(
data
:
DataFrame
[],
matcher
:
FieldMatcher
,
reducerId
:
ReducerID
[]):
DataFrame
[]
{
const
calculators
=
fieldReducers
.
list
(
reducerId
);
const
reducers
=
calculators
.
map
(
c
=>
c
.
id
);
const
processed
:
DataFrame
[]
=
[];
for
(
const
series
of
data
)
{
const
fields
:
Field
[]
=
[];
for
(
const
field
of
series
.
fields
)
{
if
(
matcher
(
field
,
series
,
data
))
{
const
results
=
reduceField
({
field
,
reducers
,
});
for
(
const
reducer
of
reducers
)
{
const
value
=
results
[
reducer
];
const
copy
=
{
...
field
,
values
:
new
ArrayVector
([
value
]),
};
copy
.
state
=
undefined
;
if
(
reducers
.
length
>
1
)
{
if
(
!
copy
.
labels
)
{
copy
.
labels
=
{};
}
copy
.
labels
[
'reducer'
]
=
fieldReducers
.
get
(
reducer
).
name
;
}
fields
.
push
(
copy
);
}
}
}
if
(
fields
.
length
)
{
processed
.
push
({
...
series
,
fields
,
length
:
1
,
// always one row
});
}
}
return
processed
;
}
packages/grafana-e2e-selectors/src/selectors/components.ts
View file @
f8e0adb1
...
...
@@ -110,6 +110,7 @@ export const Components = {
},
Transforms
:
{
Reduce
:
{
modeLabel
:
'Transform mode label'
,
calculationsLabel
:
'Transform calculations label'
,
},
},
...
...
public/app/core/components/TransformersUI/ReduceTransformerEditor.tsx
View file @
f8e0adb1
import
React
from
'react'
;
import
{
StatsPicker
}
from
'@grafana/ui'
;
import
React
,
{
useCallback
}
from
'react'
;
import
{
StatsPicker
,
Select
,
LegacyForms
}
from
'@grafana/ui'
;
import
{
DataTransformerID
,
ReducerID
,
standardTransformers
,
TransformerRegistyItem
,
TransformerUIProps
,
SelectableValue
,
}
from
'@grafana/data'
;
import
{
ReduceTransformerOptions
}
from
'@grafana/data/src/transformations/transformers/reduce'
;
import
{
ReduceTransformerOptions
,
ReduceTransformerMode
}
from
'@grafana/data/src/transformations/transformers/reduce'
;
import
{
selectors
}
from
'@grafana/e2e-selectors'
;
// TODO: Minimal implementation, needs some <3
...
...
@@ -16,27 +17,87 @@ export const ReduceTransformerEditor: React.FC<TransformerUIProps<ReduceTransfor
options
,
onChange
,
})
=>
{
const
modes
:
Array
<
SelectableValue
<
ReduceTransformerMode
>>
=
[
{
label
:
'Series to rows'
,
value
:
ReduceTransformerMode
.
SeriesToRows
,
description
:
'Create a table with one row for each series value'
,
},
{
label
:
'Reduce fields'
,
value
:
ReduceTransformerMode
.
ReduceFields
,
description
:
'Collapse each field into a single value'
,
},
];
const
onSelectMode
=
useCallback
(
(
value
:
SelectableValue
<
ReduceTransformerMode
>
)
=>
{
const
mode
=
value
.
value
!
;
onChange
({
...
options
,
mode
,
includeTimeField
:
mode
===
ReduceTransformerMode
.
ReduceFields
?
!!
options
.
includeTimeField
:
false
,
});
},
[
onChange
,
options
]
);
const
onToggleTime
=
useCallback
(()
=>
{
onChange
({
...
options
,
includeTimeField
:
!
options
.
includeTimeField
,
});
},
[
onChange
,
options
]);
return
(
<
div
className=
"gf-form-inline"
>
<
div
className=
"gf-form gf-form--grow"
>
<
div
className=
"gf-form-label width-8"
aria
-
label=
{
selectors
.
components
.
Transforms
.
Reduce
.
calculationsLabel
}
>
Calculations
<>
<
div
>
<
div
className=
"gf-form gf-form--grow"
>
<
div
className=
"gf-form-label width-8"
aria
-
label=
{
selectors
.
components
.
Transforms
.
Reduce
.
modeLabel
}
>
Mode
</
div
>
<
Select
options=
{
modes
}
value=
{
modes
.
find
(
v
=>
v
.
value
===
options
.
mode
)
||
modes
[
0
]
}
onChange=
{
onSelectMode
}
menuPlacement=
"bottom"
className=
"flex-grow-1"
/>
</
div
>
<
StatsPicker
className=
"flex-grow-1"
placeholder=
"Choose Stat"
allowMultiple
stats=
{
options
.
reducers
||
[]
}
onChange=
{
stats
=>
{
onChange
({
...
options
,
reducers
:
stats
as
ReducerID
[],
});
}
}
menuPlacement=
"bottom"
/>
</
div
>
</
div
>
<
div
className=
"gf-form-inline"
>
<
div
className=
"gf-form gf-form--grow"
>
<
div
className=
"gf-form-label width-8"
aria
-
label=
{
selectors
.
components
.
Transforms
.
Reduce
.
calculationsLabel
}
>
Calculations
</
div
>
<
StatsPicker
className=
"flex-grow-1"
placeholder=
"Choose Stat"
allowMultiple
stats=
{
options
.
reducers
||
[]
}
onChange=
{
stats
=>
{
onChange
({
...
options
,
reducers
:
stats
as
ReducerID
[],
});
}
}
menuPlacement=
"bottom"
/>
</
div
>
</
div
>
{
options
.
mode
===
ReduceTransformerMode
.
ReduceFields
&&
(
<
div
className=
"gf-form-inline"
>
<
div
className=
"gf-form"
>
<
LegacyForms
.
Switch
label=
"Include time"
labelClass=
"width-8"
checked=
{
!!
options
.
includeTimeField
}
onChange=
{
onToggleTime
}
/>
</
div
>
</
div
>
)
}
</>
);
};
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment