mirror of https://github.com/grafana/grafana
Transformations: Add Group to Nested Tables Transformation (#79952)
* Stub group to subframe transformation
* Get proper field grouping
* Mostly working but fields not displaying 😭
* Fix display processing in nested tables
* Modularize and start merging groupBy and groupToSubrame
* Get this working
* Prettier
* Typing things
* More types
* Add option for showing subframe table headers
* Prettier
* Get tests going
* Update tests
* Fix naming and add icons
* Betterer fix
* Prettier
* Fix CSS object syntax
* Prettier
* Stub alert for calcs with grouping, start renaming
* Add logic to show warning message for calculations
* Add calc warning
* Renaming and feature flag
* Rename images
* Prettier
* Fix tests
* Update feature toggle
* Fix error showing extra blank row
* minor code cleanup
---------
Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
pull/82096/head
parent
26bc87b60e
commit
756cd3c28b
@ -0,0 +1,243 @@ |
|||||||
|
import { toDataFrame } from '../../dataframe/processDataFrame'; |
||||||
|
import { DataTransformerConfig, Field, FieldType } from '../../types'; |
||||||
|
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; |
||||||
|
import { ReducerID } from '../fieldReducer'; |
||||||
|
import { transformDataFrame } from '../transformDataFrame'; |
||||||
|
|
||||||
|
import { GroupByOperationID, GroupByTransformerOptions } from './groupBy'; |
||||||
|
import { groupToNestedTable, GroupToNestedTableTransformerOptions } from './groupToNestedTable'; |
||||||
|
import { DataTransformerID } from './ids'; |
||||||
|
|
||||||
|
describe('GroupToSubframe transformer', () => { |
||||||
|
beforeAll(() => { |
||||||
|
mockTransformationsRegistry([groupToNestedTable]); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should group values by message and place values in subframe', async () => { |
||||||
|
const testSeries = toDataFrame({ |
||||||
|
name: 'A', |
||||||
|
fields: [ |
||||||
|
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000, 7000, 8000] }, |
||||||
|
{ name: 'message', type: FieldType.string, values: ['one', 'two', 'two', 'three', 'three', 'three'] }, |
||||||
|
{ name: 'values', type: FieldType.string, values: [1, 2, 2, 3, 3, 3] }, |
||||||
|
], |
||||||
|
}); |
||||||
|
|
||||||
|
const cfg: DataTransformerConfig<GroupToNestedTableTransformerOptions> = { |
||||||
|
id: DataTransformerID.groupToNestedTable, |
||||||
|
options: { |
||||||
|
fields: { |
||||||
|
message: { |
||||||
|
operation: GroupByOperationID.groupBy, |
||||||
|
aggregations: [], |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
await expect(transformDataFrame([cfg], [testSeries])).toEmitValuesWith((received) => { |
||||||
|
const result = received[0]; |
||||||
|
const expected: Field[] = [ |
||||||
|
{ |
||||||
|
name: 'message', |
||||||
|
type: FieldType.string, |
||||||
|
config: {}, |
||||||
|
values: ['one', 'two', 'three'], |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'Nested frames', |
||||||
|
type: FieldType.nestedFrames, |
||||||
|
config: {}, |
||||||
|
values: [ |
||||||
|
[ |
||||||
|
{ |
||||||
|
meta: { custom: { noHeader: false } }, |
||||||
|
length: 1, |
||||||
|
fields: [ |
||||||
|
{ name: 'time', type: 'time', config: {}, values: [3000] }, |
||||||
|
{ name: 'values', type: 'string', config: {}, values: [1] }, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
[ |
||||||
|
{ |
||||||
|
meta: { custom: { noHeader: false } }, |
||||||
|
length: 2, |
||||||
|
fields: [ |
||||||
|
{ |
||||||
|
name: 'time', |
||||||
|
type: 'time', |
||||||
|
config: {}, |
||||||
|
values: [4000, 5000], |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'values', |
||||||
|
type: 'string', |
||||||
|
config: {}, |
||||||
|
values: [2, 2], |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
[ |
||||||
|
{ |
||||||
|
meta: { custom: { noHeader: false } }, |
||||||
|
length: 3, |
||||||
|
fields: [ |
||||||
|
{ |
||||||
|
name: 'time', |
||||||
|
type: 'time', |
||||||
|
config: {}, |
||||||
|
values: [6000, 7000, 8000], |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'values', |
||||||
|
type: 'string', |
||||||
|
config: {}, |
||||||
|
values: [3, 3, 3], |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
], |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
expect(result[0].fields).toEqual(expected); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should group by message, compute a few calculations for each group of values, and place other values in a subframe', async () => { |
||||||
|
const testSeries = toDataFrame({ |
||||||
|
name: 'A', |
||||||
|
fields: [ |
||||||
|
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000, 7000, 8000] }, |
||||||
|
{ name: 'message', type: FieldType.string, values: ['one', 'two', 'two', 'three', 'three', 'three'] }, |
||||||
|
{ name: 'values', type: FieldType.number, values: [1, 2, 2, 3, 3, 3] }, |
||||||
|
{ name: 'intVal', type: FieldType.number, values: [1, 2, 3, 4, 5, 6] }, |
||||||
|
{ name: 'floatVal', type: FieldType.number, values: [1.1, 2.3, 3.6, 4.8, 5.7, 6.9] }, |
||||||
|
], |
||||||
|
}); |
||||||
|
|
||||||
|
const cfg: DataTransformerConfig<GroupByTransformerOptions> = { |
||||||
|
id: DataTransformerID.groupToNestedTable, |
||||||
|
options: { |
||||||
|
fields: { |
||||||
|
message: { |
||||||
|
operation: GroupByOperationID.groupBy, |
||||||
|
aggregations: [], |
||||||
|
}, |
||||||
|
values: { |
||||||
|
operation: GroupByOperationID.aggregate, |
||||||
|
aggregations: [ReducerID.sum], |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
await expect(transformDataFrame([cfg], [testSeries])).toEmitValuesWith((received) => { |
||||||
|
const result = received[0]; |
||||||
|
const expected: Field[] = [ |
||||||
|
{ |
||||||
|
name: 'message', |
||||||
|
type: FieldType.string, |
||||||
|
config: {}, |
||||||
|
values: ['one', 'two', 'three'], |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'values (sum)', |
||||||
|
values: [1, 4, 9], |
||||||
|
type: FieldType.number, |
||||||
|
config: {}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
config: {}, |
||||||
|
name: 'Nested frames', |
||||||
|
type: FieldType.nestedFrames, |
||||||
|
values: [ |
||||||
|
[ |
||||||
|
{ |
||||||
|
meta: { custom: { noHeader: false } }, |
||||||
|
length: 1, |
||||||
|
fields: [ |
||||||
|
{ |
||||||
|
name: 'time', |
||||||
|
type: 'time', |
||||||
|
config: {}, |
||||||
|
values: [3000], |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'intVal', |
||||||
|
type: 'number', |
||||||
|
config: {}, |
||||||
|
values: [1], |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'floatVal', |
||||||
|
type: 'number', |
||||||
|
config: {}, |
||||||
|
values: [1.1], |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
[ |
||||||
|
{ |
||||||
|
meta: { custom: { noHeader: false } }, |
||||||
|
length: 2, |
||||||
|
fields: [ |
||||||
|
{ |
||||||
|
name: 'time', |
||||||
|
type: 'time', |
||||||
|
config: {}, |
||||||
|
values: [4000, 5000], |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'intVal', |
||||||
|
type: 'number', |
||||||
|
config: {}, |
||||||
|
values: [2, 3], |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'floatVal', |
||||||
|
type: 'number', |
||||||
|
config: {}, |
||||||
|
values: [2.3, 3.6], |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
[ |
||||||
|
{ |
||||||
|
meta: { custom: { noHeader: false } }, |
||||||
|
length: 3, |
||||||
|
fields: [ |
||||||
|
{ |
||||||
|
name: 'time', |
||||||
|
type: 'time', |
||||||
|
config: {}, |
||||||
|
values: [6000, 7000, 8000], |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'intVal', |
||||||
|
type: 'number', |
||||||
|
config: {}, |
||||||
|
values: [4, 5, 6], |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'floatVal', |
||||||
|
type: 'number', |
||||||
|
config: {}, |
||||||
|
values: [4.8, 5.7, 6.9], |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
], |
||||||
|
], |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
expect(result[0].fields).toEqual(expected); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,235 @@ |
|||||||
|
import { map } from 'rxjs/operators'; |
||||||
|
|
||||||
|
import { guessFieldTypeForField } from '../../dataframe/processDataFrame'; |
||||||
|
import { getFieldDisplayName } from '../../field/fieldState'; |
||||||
|
import { DataFrame, Field, FieldType } from '../../types/dataFrame'; |
||||||
|
import { DataTransformerInfo } from '../../types/transformations'; |
||||||
|
import { ReducerID, reduceField } from '../fieldReducer'; |
||||||
|
|
||||||
|
import { GroupByFieldOptions, createGroupedFields, groupValuesByKey } from './groupBy'; |
||||||
|
import { DataTransformerID } from './ids'; |
||||||
|
|
||||||
|
export const SHOW_NESTED_HEADERS_DEFAULT = true; |
||||||
|
|
||||||
|
export enum GroupByOperationID { |
||||||
|
aggregate = 'aggregate', |
||||||
|
groupBy = 'groupby', |
||||||
|
} |
||||||
|
|
||||||
|
export interface GroupToNestedTableTransformerOptions { |
||||||
|
showSubframeHeaders?: boolean; |
||||||
|
fields: Record<string, GroupByFieldOptions>; |
||||||
|
} |
||||||
|
|
||||||
|
interface FieldMap { |
||||||
|
[key: string]: Field; |
||||||
|
} |
||||||
|
|
||||||
|
export const groupToNestedTable: DataTransformerInfo<GroupToNestedTableTransformerOptions> = { |
||||||
|
id: DataTransformerID.groupToNestedTable, |
||||||
|
name: 'Group to nested tables', |
||||||
|
description: 'Group data by a field value and create nested tables with the grouped data', |
||||||
|
defaultOptions: { |
||||||
|
showSubframeHeaders: SHOW_NESTED_HEADERS_DEFAULT, |
||||||
|
fields: {}, |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a modified copy of the series. If the transform is not or should not |
||||||
|
* be applied, just return the input series |
||||||
|
*/ |
||||||
|
operator: (options) => (source) => |
||||||
|
source.pipe( |
||||||
|
map((data) => { |
||||||
|
const hasValidConfig = Object.keys(options.fields).find( |
||||||
|
(name) => options.fields[name].operation === GroupByOperationID.groupBy |
||||||
|
); |
||||||
|
if (!hasValidConfig) { |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
const processed: DataFrame[] = []; |
||||||
|
|
||||||
|
for (const frame of data) { |
||||||
|
// Create a list of fields to group on
|
||||||
|
// If there are none we skip the rest
|
||||||
|
const groupByFields: Field[] = frame.fields.filter((field) => shouldGroupOnField(field, options)); |
||||||
|
if (groupByFields.length === 0) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// Group the values by fields and groups so we can get all values for a
|
||||||
|
// group for a given field.
|
||||||
|
const valuesByGroupKey = groupValuesByKey(frame, groupByFields); |
||||||
|
|
||||||
|
// Add the grouped fields to the resulting fields of the transformation
|
||||||
|
const fields: Field[] = createGroupedFields(groupByFields, valuesByGroupKey); |
||||||
|
|
||||||
|
// Group data into sub frames so they will display as tables
|
||||||
|
const subFrames: DataFrame[][] = groupToSubframes(valuesByGroupKey, options); |
||||||
|
|
||||||
|
// Then for each calculations configured, compute and add a new field (column)
|
||||||
|
for (let i = 0; i < frame.fields.length; i++) { |
||||||
|
const field = frame.fields[i]; |
||||||
|
|
||||||
|
if (!shouldCalculateField(field, options)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
const fieldName = getFieldDisplayName(field); |
||||||
|
const aggregations = options.fields[fieldName].aggregations; |
||||||
|
const valuesByAggregation: Record<string, unknown[]> = {}; |
||||||
|
|
||||||
|
valuesByGroupKey.forEach((value) => { |
||||||
|
const fieldWithValuesForGroup = value[fieldName]; |
||||||
|
const results = reduceField({ |
||||||
|
field: fieldWithValuesForGroup, |
||||||
|
reducers: aggregations, |
||||||
|
}); |
||||||
|
|
||||||
|
for (const aggregation of aggregations) { |
||||||
|
if (!Array.isArray(valuesByAggregation[aggregation])) { |
||||||
|
valuesByAggregation[aggregation] = []; |
||||||
|
} |
||||||
|
valuesByAggregation[aggregation].push(results[aggregation]); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
for (const aggregation of aggregations) { |
||||||
|
const aggregationField: Field = { |
||||||
|
name: `${fieldName} (${aggregation})`, |
||||||
|
values: valuesByAggregation[aggregation], |
||||||
|
type: FieldType.other, |
||||||
|
config: {}, |
||||||
|
}; |
||||||
|
|
||||||
|
aggregationField.type = detectFieldType(aggregation, field, aggregationField); |
||||||
|
fields.push(aggregationField); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fields.push({ |
||||||
|
config: {}, |
||||||
|
name: 'Nested frames', |
||||||
|
type: FieldType.nestedFrames, |
||||||
|
values: subFrames, |
||||||
|
}); |
||||||
|
|
||||||
|
processed.push({ |
||||||
|
fields, |
||||||
|
length: valuesByGroupKey.size, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
return processed; |
||||||
|
}) |
||||||
|
), |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Given the appropriate data, create a sub-frame |
||||||
|
* which can then be displayed in a sub-table. |
||||||
|
*/ |
||||||
|
function createSubframe(fields: Field[], frameLength: number, options: GroupToNestedTableTransformerOptions) { |
||||||
|
const showHeaders = |
||||||
|
options.showSubframeHeaders === undefined ? SHOW_NESTED_HEADERS_DEFAULT : options.showSubframeHeaders; |
||||||
|
|
||||||
|
return { |
||||||
|
meta: { custom: { noHeader: !showHeaders } }, |
||||||
|
length: frameLength, |
||||||
|
fields, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Determines whether a field should be grouped on. |
||||||
|
* |
||||||
|
* @returns boolean |
||||||
|
* This will return _true_ if a field should be grouped on and _false_ if it should not. |
||||||
|
*/ |
||||||
|
const shouldGroupOnField = (field: Field, options: GroupToNestedTableTransformerOptions): boolean => { |
||||||
|
const fieldName = getFieldDisplayName(field); |
||||||
|
return options?.fields[fieldName]?.operation === GroupByOperationID.groupBy; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Determines whether field aggregations should be calculated |
||||||
|
* @returns boolean |
||||||
|
* This will return _true_ if a field should be calculated and _false_ if it should not. |
||||||
|
*/ |
||||||
|
const shouldCalculateField = (field: Field, options: GroupToNestedTableTransformerOptions): boolean => { |
||||||
|
const fieldName = getFieldDisplayName(field); |
||||||
|
return ( |
||||||
|
options?.fields[fieldName]?.operation === GroupByOperationID.aggregate && |
||||||
|
Array.isArray(options?.fields[fieldName].aggregations) && |
||||||
|
options?.fields[fieldName].aggregations.length > 0 |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Detect the type of field given the relevant aggregation. |
||||||
|
*/ |
||||||
|
const detectFieldType = (aggregation: string, sourceField: Field, targetField: Field): FieldType => { |
||||||
|
switch (aggregation) { |
||||||
|
case ReducerID.allIsNull: |
||||||
|
return FieldType.boolean; |
||||||
|
case ReducerID.last: |
||||||
|
case ReducerID.lastNotNull: |
||||||
|
case ReducerID.first: |
||||||
|
case ReducerID.firstNotNull: |
||||||
|
return sourceField.type; |
||||||
|
default: |
||||||
|
return guessFieldTypeForField(targetField) ?? FieldType.string; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Group values into subframes so that they'll be displayed |
||||||
|
* inside of a subtable. |
||||||
|
* |
||||||
|
* @param valuesByGroupKey |
||||||
|
* A mapping of group keys to their respective grouped values. |
||||||
|
* @param options |
||||||
|
* Transformation options, which are used to find ungrouped/unaggregated fields. |
||||||
|
* @returns |
||||||
|
*/ |
||||||
|
function groupToSubframes( |
||||||
|
valuesByGroupKey: Map<string, FieldMap>, |
||||||
|
options: GroupToNestedTableTransformerOptions |
||||||
|
): DataFrame[][] { |
||||||
|
const subFrames: DataFrame[][] = []; |
||||||
|
|
||||||
|
// Construct a subframe of any fields
|
||||||
|
// that aren't being group on or reduced
|
||||||
|
for (const [, value] of valuesByGroupKey) { |
||||||
|
const nestedFields: Field[] = []; |
||||||
|
|
||||||
|
for (const [fieldName, field] of Object.entries(value)) { |
||||||
|
const fieldOpts = options.fields[fieldName]; |
||||||
|
|
||||||
|
if (fieldOpts === undefined) { |
||||||
|
nestedFields.push(field); |
||||||
|
} |
||||||
|
// Depending on the configuration form state all of the following are possible
|
||||||
|
else if ( |
||||||
|
fieldOpts.aggregations === undefined || |
||||||
|
(fieldOpts.operation === GroupByOperationID.aggregate && fieldOpts.aggregations.length === 0) || |
||||||
|
fieldOpts.operation === null || |
||||||
|
fieldOpts.operation === undefined |
||||||
|
) { |
||||||
|
nestedFields.push(field); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If there are any values in the subfields
|
||||||
|
// push a new subframe with the fields
|
||||||
|
// otherwise push an empty frame
|
||||||
|
if (nestedFields.length > 0) { |
||||||
|
subFrames.push([createSubframe(nestedFields, nestedFields[0].values.length, options)]); |
||||||
|
} else { |
||||||
|
subFrames.push([createSubframe([], 0, options)]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return subFrames; |
||||||
|
} |
|
@ -0,0 +1,185 @@ |
|||||||
|
import { css } from '@emotion/css'; |
||||||
|
import React, { useCallback } from 'react'; |
||||||
|
|
||||||
|
import { |
||||||
|
DataTransformerID, |
||||||
|
ReducerID, |
||||||
|
SelectableValue, |
||||||
|
standardTransformers, |
||||||
|
TransformerRegistryItem, |
||||||
|
TransformerUIProps, |
||||||
|
TransformerCategory, |
||||||
|
GrafanaTheme2, |
||||||
|
} from '@grafana/data'; |
||||||
|
import { |
||||||
|
GroupByFieldOptions, |
||||||
|
GroupByOperationID, |
||||||
|
GroupByTransformerOptions, |
||||||
|
} from '@grafana/data/src/transformations/transformers/groupBy'; |
||||||
|
import { |
||||||
|
GroupToNestedTableTransformerOptions, |
||||||
|
SHOW_NESTED_HEADERS_DEFAULT, |
||||||
|
} from '@grafana/data/src/transformations/transformers/groupToNestedTable'; |
||||||
|
import { Stack } from '@grafana/experimental'; |
||||||
|
import { useTheme2, Select, StatsPicker, InlineField, Field, Switch, Alert } from '@grafana/ui'; |
||||||
|
|
||||||
|
import { useAllFieldNamesFromDataFrames } from '../utils'; |
||||||
|
|
||||||
|
interface FieldProps { |
||||||
|
fieldName: string; |
||||||
|
config?: GroupByFieldOptions; |
||||||
|
onConfigChange: (config: GroupByFieldOptions) => void; |
||||||
|
} |
||||||
|
|
||||||
|
export const GroupToNestedTableTransformerEditor = ({ |
||||||
|
input, |
||||||
|
options, |
||||||
|
onChange, |
||||||
|
}: TransformerUIProps<GroupToNestedTableTransformerOptions>) => { |
||||||
|
const fieldNames = useAllFieldNamesFromDataFrames(input); |
||||||
|
const showHeaders = |
||||||
|
options.showSubframeHeaders === undefined ? SHOW_NESTED_HEADERS_DEFAULT : options.showSubframeHeaders; |
||||||
|
|
||||||
|
const onConfigChange = useCallback( |
||||||
|
(fieldName: string) => (config: GroupByFieldOptions) => { |
||||||
|
onChange({ |
||||||
|
...options, |
||||||
|
fields: { |
||||||
|
...options.fields, |
||||||
|
[fieldName]: config, |
||||||
|
}, |
||||||
|
}); |
||||||
|
}, |
||||||
|
// Adding options to the dependency array causes infinite loop here.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[onChange] |
||||||
|
); |
||||||
|
|
||||||
|
const onShowFieldNamesChange = useCallback( |
||||||
|
() => { |
||||||
|
const showSubframeHeaders = |
||||||
|
options.showSubframeHeaders === undefined ? !SHOW_NESTED_HEADERS_DEFAULT : !options.showSubframeHeaders; |
||||||
|
|
||||||
|
onChange({ |
||||||
|
showSubframeHeaders, |
||||||
|
fields: { |
||||||
|
...options.fields, |
||||||
|
}, |
||||||
|
}); |
||||||
|
}, |
||||||
|
// Adding options to the dependency array causes infinite loop here.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[onChange] |
||||||
|
); |
||||||
|
|
||||||
|
// See if there's both an aggregation and grouping field configured
|
||||||
|
// for calculations. If not we display a warning because there
|
||||||
|
// needs to be a grouping for the calculation to have effect
|
||||||
|
let hasGrouping, |
||||||
|
hasAggregation = false; |
||||||
|
for (const field of Object.values(options.fields)) { |
||||||
|
if (field.aggregations.length > 0 && field.operation !== null) { |
||||||
|
hasAggregation = true; |
||||||
|
} |
||||||
|
if (field.operation === GroupByOperationID.groupBy) { |
||||||
|
hasGrouping = true; |
||||||
|
} |
||||||
|
} |
||||||
|
const showCalcAlert = hasAggregation && !hasGrouping; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Stack direction="column"> |
||||||
|
{showCalcAlert && ( |
||||||
|
<Alert title="Calculations will not have an effect if no fields are being grouped on." severity="warning" /> |
||||||
|
)} |
||||||
|
<div> |
||||||
|
{fieldNames.map((key) => ( |
||||||
|
<GroupByFieldConfiguration |
||||||
|
onConfigChange={onConfigChange(key)} |
||||||
|
fieldName={key} |
||||||
|
config={options.fields[key]} |
||||||
|
key={key} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
<Field |
||||||
|
label="Show field names in nested tables" |
||||||
|
description="If enabled nested tables will show field names as a table header" |
||||||
|
> |
||||||
|
<Switch value={showHeaders} onChange={onShowFieldNamesChange} /> |
||||||
|
</Field> |
||||||
|
</Stack> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const options = [ |
||||||
|
{ label: 'Group by', value: GroupByOperationID.groupBy }, |
||||||
|
{ label: 'Calculate', value: GroupByOperationID.aggregate }, |
||||||
|
]; |
||||||
|
|
||||||
|
export const GroupByFieldConfiguration = ({ fieldName, config, onConfigChange }: FieldProps) => { |
||||||
|
const theme = useTheme2(); |
||||||
|
const styles = getStyles(theme); |
||||||
|
|
||||||
|
const onChange = useCallback( |
||||||
|
(value: SelectableValue<GroupByOperationID | null>) => { |
||||||
|
onConfigChange({ |
||||||
|
aggregations: config?.aggregations ?? [], |
||||||
|
operation: value?.value ?? null, |
||||||
|
}); |
||||||
|
}, |
||||||
|
[config, onConfigChange] |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<InlineField className={styles.label} label={fieldName} grow shrink> |
||||||
|
<Stack gap={0.5} direction="row" wrap={false}> |
||||||
|
<div className={styles.operation}> |
||||||
|
<Select options={options} value={config?.operation} placeholder="Ignored" onChange={onChange} isClearable /> |
||||||
|
</div> |
||||||
|
|
||||||
|
{config?.operation === GroupByOperationID.aggregate && ( |
||||||
|
<StatsPicker |
||||||
|
className={styles.aggregations} |
||||||
|
placeholder="Select Stats" |
||||||
|
allowMultiple |
||||||
|
stats={config.aggregations} |
||||||
|
onChange={(stats) => { |
||||||
|
// eslint-disable-next-line
|
||||||
|
onConfigChange({ ...config, aggregations: stats as ReducerID[] }); |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</Stack> |
||||||
|
</InlineField> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => { |
||||||
|
return { |
||||||
|
label: css({ |
||||||
|
minWidth: theme.spacing(32), |
||||||
|
}), |
||||||
|
operation: css({ |
||||||
|
flexShrink: 0, |
||||||
|
height: '100%', |
||||||
|
width: theme.spacing(24), |
||||||
|
}), |
||||||
|
aggregations: css({ |
||||||
|
flexGrow: 1, |
||||||
|
}), |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export const groupToNestedTableTransformRegistryItem: TransformerRegistryItem<GroupByTransformerOptions> = { |
||||||
|
id: DataTransformerID.groupToNestedTable, |
||||||
|
editor: GroupToNestedTableTransformerEditor, |
||||||
|
transformation: standardTransformers.groupToNestedTable, |
||||||
|
name: standardTransformers.groupToNestedTable.name, |
||||||
|
description: standardTransformers.groupToNestedTable.description, |
||||||
|
categories: new Set([ |
||||||
|
TransformerCategory.Combine, |
||||||
|
TransformerCategory.CalculateNewFields, |
||||||
|
TransformerCategory.Reformat, |
||||||
|
]), |
||||||
|
}; |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 6.3 KiB |
Loading…
Reference in new issue