mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 02:10:45 -06:00
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>
This commit is contained in:
parent
26bc87b60e
commit
756cd3c28b
@ -87,6 +87,7 @@ Some features are enabled by default. You can disable these feature by setting t
|
||||
| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel |
|
||||
| `regressionTransformation` | Enables regression analysis transformation |
|
||||
| `alertingPreviewUpgrade` | Show Unified Alerting preview and upgrade page in legacy alerting |
|
||||
| `groupToNestedTableTransformation` | Enables the group to nested table transformation |
|
||||
|
||||
## Experimental feature toggles
|
||||
|
||||
|
@ -206,6 +206,13 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
|
||||
for (const nestedFrames of field.values) {
|
||||
for (let nfIndex = 0; nfIndex < nestedFrames.length; nfIndex++) {
|
||||
for (const valueField of nestedFrames[nfIndex].fields) {
|
||||
// Get display processor for nested fields
|
||||
valueField.display = getDisplayProcessor({
|
||||
field: valueField,
|
||||
theme: options.theme,
|
||||
timeZone: options.timeZone,
|
||||
});
|
||||
|
||||
valueField.state = {
|
||||
scopedVars: {
|
||||
__dataContext: {
|
||||
|
@ -9,6 +9,7 @@ import { filterByValueTransformer } from './transformers/filterByValue';
|
||||
import { formatStringTransformer } from './transformers/formatString';
|
||||
import { formatTimeTransformer } from './transformers/formatTime';
|
||||
import { groupByTransformer } from './transformers/groupBy';
|
||||
import { groupToNestedTable } from './transformers/groupToNestedTable';
|
||||
import { groupingToMatrixTransformer } from './transformers/groupingToMatrix';
|
||||
import { histogramTransformer } from './transformers/histogram';
|
||||
import { joinByFieldTransformer } from './transformers/joinByField';
|
||||
@ -53,4 +54,5 @@ export const standardTransformers = {
|
||||
convertFieldTypeTransformer,
|
||||
groupingToMatrixTransformer,
|
||||
limitTransformer,
|
||||
groupToNestedTable,
|
||||
};
|
||||
|
@ -22,6 +22,10 @@ export interface GroupByTransformerOptions {
|
||||
fields: Record<string, GroupByFieldOptions>;
|
||||
}
|
||||
|
||||
interface FieldMap {
|
||||
[key: string]: Field;
|
||||
}
|
||||
|
||||
export const groupByTransformer: DataTransformerInfo<GroupByTransformerOptions> = {
|
||||
id: DataTransformerID.groupBy,
|
||||
name: 'Group by',
|
||||
@ -75,64 +79,19 @@ export const groupByTransformer: DataTransformerInfo<GroupByTransformerOptions>
|
||||
const processed: DataFrame[] = [];
|
||||
|
||||
for (const frame of data) {
|
||||
const groupByFields: Field[] = [];
|
||||
|
||||
for (const field of frame.fields) {
|
||||
if (shouldGroupOnField(field, options)) {
|
||||
groupByFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
// 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; // No group by field in this frame, ignore the frame
|
||||
continue;
|
||||
}
|
||||
|
||||
// Group the values by fields and groups so we can get all values for a
|
||||
// group for a given field.
|
||||
const valuesByGroupKey = new Map<string, Record<string, Field>>();
|
||||
for (let rowIndex = 0; rowIndex < frame.length; rowIndex++) {
|
||||
const groupKey = String(groupByFields.map((field) => field.values[rowIndex]));
|
||||
const valuesByField = valuesByGroupKey.get(groupKey) ?? {};
|
||||
const valuesByGroupKey = groupValuesByKey(frame, groupByFields);
|
||||
|
||||
if (!valuesByGroupKey.has(groupKey)) {
|
||||
valuesByGroupKey.set(groupKey, valuesByField);
|
||||
}
|
||||
|
||||
for (let field of frame.fields) {
|
||||
const fieldName = getFieldDisplayName(field);
|
||||
|
||||
if (!valuesByField[fieldName]) {
|
||||
valuesByField[fieldName] = {
|
||||
name: fieldName,
|
||||
type: field.type,
|
||||
config: { ...field.config },
|
||||
values: [],
|
||||
};
|
||||
}
|
||||
|
||||
valuesByField[fieldName].values.push(field.values[rowIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
const fields: Field[] = [];
|
||||
|
||||
for (const field of groupByFields) {
|
||||
const values: unknown[] = [];
|
||||
const fieldName = getFieldDisplayName(field);
|
||||
|
||||
valuesByGroupKey.forEach((value) => {
|
||||
values.push(value[fieldName].values[0]);
|
||||
});
|
||||
|
||||
fields.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
config: {
|
||||
...field.config,
|
||||
},
|
||||
values: values,
|
||||
});
|
||||
}
|
||||
// Add the grouped fields to the resulting fields of the transformation
|
||||
const fields: Field[] = createGroupedFields(groupByFields, valuesByGroupKey);
|
||||
|
||||
// Then for each calculations configured, compute and add a new field (column)
|
||||
for (const field of frame.fields) {
|
||||
@ -197,7 +156,7 @@ const shouldCalculateField = (field: Field, options: GroupByTransformerOptions):
|
||||
);
|
||||
};
|
||||
|
||||
const detectFieldType = (aggregation: string, sourceField: Field, targetField: Field): FieldType => {
|
||||
function detectFieldType(aggregation: string, sourceField: Field, targetField: Field): FieldType {
|
||||
switch (aggregation) {
|
||||
case ReducerID.allIsNull:
|
||||
return FieldType.boolean;
|
||||
@ -209,4 +168,75 @@ const detectFieldType = (aggregation: string, sourceField: Field, targetField: F
|
||||
default:
|
||||
return guessFieldTypeForField(targetField) ?? FieldType.string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups values together by key. This will create a mapping of strings
|
||||
* to _FieldMaps_ that will then be used to group values on.
|
||||
*
|
||||
* @param frame
|
||||
* The dataframe containing the data to group.
|
||||
* @param groupByFields
|
||||
* An array of fields to group on.
|
||||
*/
|
||||
export function groupValuesByKey(frame: DataFrame, groupByFields: Field[]) {
|
||||
const valuesByGroupKey = new Map<string, FieldMap>();
|
||||
|
||||
for (let rowIndex = 0; rowIndex < frame.length; rowIndex++) {
|
||||
const groupKey = String(groupByFields.map((field) => field.values[rowIndex]));
|
||||
const valuesByField = valuesByGroupKey.get(groupKey) ?? {};
|
||||
|
||||
if (!valuesByGroupKey.has(groupKey)) {
|
||||
valuesByGroupKey.set(groupKey, valuesByField);
|
||||
}
|
||||
|
||||
for (let field of frame.fields) {
|
||||
const fieldName = getFieldDisplayName(field);
|
||||
|
||||
if (!valuesByField[fieldName]) {
|
||||
valuesByField[fieldName] = {
|
||||
name: fieldName,
|
||||
type: field.type,
|
||||
config: { ...field.config },
|
||||
values: [],
|
||||
};
|
||||
}
|
||||
|
||||
valuesByField[fieldName].values.push(field.values[rowIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
return valuesByGroupKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new fields which will be used to display grouped values.
|
||||
*
|
||||
* @param groupByFields
|
||||
* @param valuesByGroupKey
|
||||
* @returns
|
||||
* Returns an array of fields that have been grouped.
|
||||
*/
|
||||
export function createGroupedFields(groupByFields: Field[], valuesByGroupKey: Map<string, FieldMap>): Field[] {
|
||||
const fields: Field[] = [];
|
||||
|
||||
for (const field of groupByFields) {
|
||||
const values: unknown[] = [];
|
||||
const fieldName = getFieldDisplayName(field);
|
||||
|
||||
valuesByGroupKey.forEach((value) => {
|
||||
values.push(value[fieldName].values[0]);
|
||||
});
|
||||
|
||||
fields.push({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
config: {
|
||||
...field.config,
|
||||
},
|
||||
values,
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -40,4 +40,5 @@ export enum DataTransformerID {
|
||||
formatTime = 'formatTime',
|
||||
formatString = 'formatString',
|
||||
regression = 'regression',
|
||||
groupToNestedTable = 'groupToNestedTable',
|
||||
}
|
||||
|
@ -173,4 +173,5 @@ export interface FeatureToggles {
|
||||
alertingSaveStatePeriodic?: boolean;
|
||||
promQLScope?: boolean;
|
||||
nodeGraphDotLayout?: boolean;
|
||||
groupToNestedTableTransformation?: boolean;
|
||||
}
|
||||
|
@ -1310,5 +1310,13 @@ var (
|
||||
Owner: grafanaObservabilityTracesAndProfilingSquad,
|
||||
Created: time.Date(2024, time.January, 2, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
Name: "groupToNestedTableTransformation",
|
||||
Description: "Enables the group to nested table transformation",
|
||||
Stage: FeatureStagePublicPreview,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDatavizSquad,
|
||||
Created: time.Date(2024, time.February, 5, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -154,3 +154,4 @@ onPremToCloudMigrations,experimental,@grafana/grafana-operator-experience-squad,
|
||||
alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,2024-01-22,false,false,false
|
||||
promQLScope,experimental,@grafana/observability-metrics,2024-01-29,false,false,false
|
||||
nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,2024-01-02,false,false,true
|
||||
groupToNestedTableTransformation,preview,@grafana/dataviz-squad,2024-02-05,false,false,true
|
||||
|
|
@ -626,4 +626,8 @@ const (
|
||||
// FlagNodeGraphDotLayout
|
||||
// Changed the layout algorithm for the node graph
|
||||
FlagNodeGraphDotLayout = "nodeGraphDotLayout"
|
||||
|
||||
// FlagGroupToNestedTableTransformation
|
||||
// Enables the group to nested table transformation
|
||||
FlagGroupToNestedTableTransformation = "groupToNestedTableTransformation"
|
||||
)
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
GroupByOperationID,
|
||||
GroupByTransformerOptions,
|
||||
} from '@grafana/data/src/transformations/transformers/groupBy';
|
||||
import { useTheme2, Select, StatsPicker, InlineField, Stack } from '@grafana/ui';
|
||||
import { useTheme2, Select, StatsPicker, InlineField, Stack, Alert } from '@grafana/ui';
|
||||
|
||||
import { getTransformationContent } from '../docs/getTransformationContent';
|
||||
import { useAllFieldNamesFromDataFrames } from '../utils';
|
||||
@ -49,8 +49,28 @@ export const GroupByTransformerEditor = ({
|
||||
[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 (
|
||||
<div>
|
||||
<Stack direction="column">
|
||||
{showCalcAlert && (
|
||||
<Alert title="Calculations will not have an effect if no fields are being grouped on." severity="warning" />
|
||||
)}
|
||||
{fieldNames.map((key) => (
|
||||
<GroupByFieldConfiguration
|
||||
onConfigChange={onConfigChange(key)}
|
||||
@ -59,7 +79,7 @@ export const GroupByTransformerEditor = ({
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
]),
|
||||
};
|
@ -12,6 +12,7 @@ import { filterFramesByRefIdTransformRegistryItem } from './editors/FilterByRefI
|
||||
import { formatStringTransformerRegistryItem } from './editors/FormatStringTransformerEditor';
|
||||
import { formatTimeTransformerRegistryItem } from './editors/FormatTimeTransformerEditor';
|
||||
import { groupByTransformRegistryItem } from './editors/GroupByTransformerEditor';
|
||||
import { groupToNestedTableTransformRegistryItem } from './editors/GroupToNestedTableTransformerEditor';
|
||||
import { groupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor';
|
||||
import { histogramTransformRegistryItem } from './editors/HistogramTransformerEditor';
|
||||
import { joinByFieldTransformerRegistryItem } from './editors/JoinByFieldTransformerEditor';
|
||||
@ -64,6 +65,7 @@ export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> =
|
||||
partitionByValuesTransformRegistryItem,
|
||||
...(config.featureToggles.formatString ? [formatStringTransformerRegistryItem] : []),
|
||||
...(config.featureToggles.regressionTransformation ? [regressionTransformerRegistryItem] : []),
|
||||
...(config.featureToggles.groupToNestedTableTransformation ? [groupToNestedTableTransformRegistryItem] : []),
|
||||
formatTimeTransformerRegistryItem,
|
||||
timeSeriesTableTransformRegistryItem,
|
||||
];
|
||||
|
52
public/img/transformations/dark/groupToNestedTable.svg
Normal file
52
public/img/transformations/dark/groupToNestedTable.svg
Normal file
@ -0,0 +1,52 @@
|
||||
<svg width="106" height="50" viewBox="0 0 106 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1899_468)">
|
||||
<path d="M0 0.800641V10H24V1.05456e-06H1.00292L0.000198677 0L0 0.800641Z" fill="url(#paint0_linear_1899_468)"/>
|
||||
<path d="M24 13H0V22.5H24V13Z" fill="#84AFF1"/>
|
||||
<path d="M24 25.75H0V35.25H24V25.75Z" fill="#84AFF1"/>
|
||||
<path d="M24 38.5H0V48H24V38.5Z" fill="#84AFF1"/>
|
||||
<path d="M13 14.6179H11.5716L10 15.5378V16.7917L11.4253 15.9714H11.4627V20.6179H13V14.6179Z" fill="#24292E"/>
|
||||
<path d="M9.83733 33.5H14.25V32.3468H11.8341V32.3092L12.5472 31.6561C13.8396 30.5376 14.1772 29.9682 14.1772 29.289C14.1772 28.2225 13.3011 27.5 11.9476 27.5C10.629 27.5 9.7471 28.2543 9.75001 29.4595H11.1151C11.1151 28.9249 11.447 28.6156 11.9418 28.6156C12.4279 28.6156 12.7801 28.9133 12.7801 29.4017C12.7801 29.8439 12.5006 30.1445 12.0116 30.5809L9.83733 32.4711V33.5Z" fill="#24292E"/>
|
||||
<path d="M9.83733 46.25H14.25V45.0968H11.8341V45.0592L12.5472 44.4061C13.8396 43.2876 14.1772 42.7182 14.1772 42.039C14.1772 40.9725 13.3011 40.25 11.9476 40.25C10.629 40.25 9.7471 41.0043 9.75001 42.2095H11.1151C11.1151 41.6749 11.447 41.3656 11.9418 41.3656C12.4279 41.3656 12.7801 41.6633 12.7801 42.1517C12.7801 42.5939 12.5006 42.8945 12.0116 43.3309L9.83733 45.2211V46.25Z" fill="#24292E"/>
|
||||
</g>
|
||||
<g clip-path="url(#clip1_1899_468)">
|
||||
<path d="M28 0.800641V10H52V1.05456e-06H29.0029L28.0002 0L28 0.800641Z" fill="url(#paint1_linear_1899_468)"/>
|
||||
<path d="M52 13H28V22.5H52V13Z" fill="#84AFF1"/>
|
||||
<path d="M52 25.75H28V35.25H52V25.75Z" fill="#84AFF1"/>
|
||||
<path d="M52 38.5H28V48H52V38.5Z" fill="#84AFF1"/>
|
||||
<path d="M39.9792 20.875C41.3085 20.875 42.2528 20.1507 42.25 19.1355C42.2528 18.4168 41.7958 17.9035 40.9208 17.798V17.7524C41.5771 17.6554 42.0423 17.202 42.0395 16.5461C42.0423 15.5936 41.206 14.875 39.9903 14.875C38.7663 14.875 37.8746 15.605 37.8635 16.6431H39.1734C39.1845 16.2438 39.5334 15.9758 39.9903 15.9758C40.4251 15.9758 40.7214 16.2467 40.7186 16.6374C40.7214 17.0423 40.3725 17.3218 39.8685 17.3218H39.3174V18.3256H39.8685C40.4417 18.3256 40.8155 18.6164 40.81 19.0271C40.8155 19.4406 40.4721 19.7286 39.9848 19.7286C39.5001 19.7286 39.1429 19.4634 39.1291 19.0784H37.75C37.7611 20.1336 38.6832 20.875 39.9792 20.875Z" fill="#24292E"/>
|
||||
<path d="M37.5 32.4453V31.4463L40.0893 27.5H41.7308V31.4287H42.5V32.4453H41.7308V33.5H40.477V32.4453H37.5ZM40.5012 31.4287V28.8828H40.4528L38.8204 31.3818V31.4287H40.5012Z" fill="#24292E"/>
|
||||
<path d="M40.2102 46.25C38.8025 46.25 37.7766 45.5188 37.75 44.4957H39.1444C39.1776 44.9552 39.6391 45.2673 40.2102 45.2673C40.8841 45.2673 41.3655 44.8425 41.3655 44.2269C41.3655 43.6055 40.8742 43.1749 40.1902 43.172C39.7885 43.172 39.3835 43.3251 39.1743 43.5708L37.8961 43.3714L38.2181 40.25H42.3748V41.2731H39.4034L39.2274 42.7558H39.2673C39.5063 42.4639 40.0209 42.2471 40.6185 42.2471C41.8436 42.2471 42.7533 43.0621 42.75 44.1951C42.7533 45.3916 41.7241 46.25 40.2102 46.25Z" fill="#24292E"/>
|
||||
</g>
|
||||
<path d="M80 0.800641V10H104V1.05456e-06H81.0029L80.0002 0L80 0.800641Z" fill="url(#paint2_linear_1899_468)"/>
|
||||
<path d="M104 17H80V24H104V17Z" fill="#84AFF1"/>
|
||||
<path d="M104 33H80V40H104V33Z" fill="#84AFF1"/>
|
||||
<path d="M104 41H80V48H104V41Z" fill="#84AFF1"/>
|
||||
<path d="M91.8577 23C92.9654 23 93.7523 22.3964 93.75 21.5504C93.7523 20.9515 93.3715 20.5238 92.6423 20.4358V20.3978C93.1892 20.317 93.5769 19.9392 93.5746 19.3926C93.5769 18.5989 92.88 18 91.8669 18C90.8469 18 90.1038 18.6084 90.0946 19.4734H91.1862C91.1954 19.1407 91.4861 18.9173 91.8669 18.9173C92.2292 18.9173 92.4761 19.1431 92.4738 19.4686C92.4761 19.8061 92.1854 20.039 91.7654 20.039H91.3062V20.8755H91.7654C92.2431 20.8755 92.5546 21.1179 92.55 21.4601C92.5546 21.8047 92.2685 22.0447 91.8623 22.0447C91.4585 22.0447 91.1608 21.8237 91.1492 21.5029H90C90.0092 22.3821 90.7777 23 91.8577 23Z" fill="#24292E"/>
|
||||
<path d="M90 38.1211V37.2886L92.1578 34H93.5256V37.2739H94.1667V38.1211H93.5256V39H92.4808V38.1211H90ZM92.501 37.2739V35.1523H92.4606L91.1003 37.2349V37.2739H92.501Z" fill="#24292E"/>
|
||||
<path d="M92.0501 47C90.877 47 90.0221 46.3907 90 45.5381H91.162C91.1897 45.921 91.5743 46.1811 92.0501 46.1811C92.6118 46.1811 93.0129 45.8271 93.0129 45.3141C93.0129 44.7962 92.6035 44.4374 92.0335 44.435C91.6988 44.435 91.3612 44.5626 91.1869 44.7673L90.1217 44.6012L90.3901 42H93.854V42.8526H91.3778L91.2312 44.0881H91.2644C91.4636 43.8449 91.8924 43.6643 92.3904 43.6643C93.4113 43.6643 94.1694 44.3434 94.1667 45.2876C94.1694 46.2847 93.3117 47 92.0501 47Z" fill="#24292E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M104 33H80V47.579H104V33ZM78 28V50H106V28L78 28Z" fill="#CCCCDC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M104 17H80V24H104V17ZM78 12V26H106V12H78Z" fill="#CCCCDC"/>
|
||||
<path d="M65.9067 31.75C66.6011 31.75 71.9327 27.75 71.9327 25.75C71.9327 23.75 66.7357 19.75 65.9067 19.75C65.0777 19.75 64.4023 20.25 64.4023 21.2256C64.4023 22.2012 67.9067 24.6706 67.9067 24.6706C67.9067 24.6706 60.2539 24 60 24.6706C59.7461 25.3411 59.7461 26.1589 60 26.8294C60.2539 27.5 67.9067 26.8294 67.9067 26.8294C67.9067 26.8294 64.4023 29.5 64.4023 30.2801C64.4023 31.0603 65.2123 31.75 65.9067 31.75Z" fill="#CCCCDC"/>
|
||||
<path d="M81.25 13H80.1787L79 13.6899V14.6304L80.069 14.0151H80.097V17.5H81.25V13Z" fill="#24292E"/>
|
||||
<path d="M79.0655 33.5H82.375V32.6351H80.5631V32.6069L81.0979 32.1171C82.0672 31.2782 82.3204 30.8512 82.3204 30.3418C82.3204 29.5419 81.6633 29 80.6482 29C79.6593 29 78.9978 29.5658 79 30.4697H80.0239C80.0239 30.0686 80.2727 29.8367 80.6438 29.8367C81.0084 29.8367 81.2726 30.06 81.2726 30.4263C81.2726 30.7579 81.063 30.9834 80.6962 31.3107L79.0655 32.7283V33.5Z" fill="#24292E"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1899_468" x1="0.00781255" y1="5.004" x2="24.0078" y2="5.004" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1899_468" x1="28.0078" y1="5.004" x2="52.0078" y2="5.004" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1899_468" x1="80.0078" y1="5.004" x2="104.008" y2="5.004" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1899_468">
|
||||
<rect width="24" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_1899_468">
|
||||
<rect width="24" height="48" fill="white" transform="translate(28)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 6.3 KiB |
40
public/img/transformations/disabled/groupToNestedTable.svg
Normal file
40
public/img/transformations/disabled/groupToNestedTable.svg
Normal file
@ -0,0 +1,40 @@
|
||||
<svg width="106" height="50" viewBox="0 0 106 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1899_35965)">
|
||||
<path d="M0 0.800641V10H24V1.05456e-06H1.00292L0.000198677 0L0 0.800641Z" fill="#CCCCDC" fill-opacity="0.04"/>
|
||||
<path d="M24 13H0V22.5H24V13Z" fill="#CCCCDC" fill-opacity="0.12"/>
|
||||
<path d="M24 25.75H0V35.25H24V25.75Z" fill="#CCCCDC" fill-opacity="0.12"/>
|
||||
<path d="M24 38.5H0V48H24V38.5Z" fill="#CCCCDC" fill-opacity="0.12"/>
|
||||
<path d="M13 14.6179H11.5716L10 15.5378V16.7917L11.4253 15.9714H11.4627V20.6179H13V14.6179Z" fill="#CCCCDC" fill-opacity="0.4"/>
|
||||
<path d="M9.83733 33.5H14.25V32.3468H11.8341V32.3092L12.5472 31.6561C13.8396 30.5376 14.1772 29.9682 14.1772 29.289C14.1772 28.2225 13.3011 27.5 11.9476 27.5C10.629 27.5 9.7471 28.2543 9.75001 29.4595H11.1151C11.1151 28.9249 11.447 28.6156 11.9418 28.6156C12.4279 28.6156 12.7801 28.9133 12.7801 29.4017C12.7801 29.8439 12.5006 30.1445 12.0116 30.5809L9.83733 32.4711V33.5Z" fill="#CCCCDC" fill-opacity="0.4"/>
|
||||
<path d="M9.83733 46.25H14.25V45.0968H11.8341V45.0592L12.5472 44.4061C13.8396 43.2876 14.1772 42.7182 14.1772 42.039C14.1772 40.9725 13.3011 40.25 11.9476 40.25C10.629 40.25 9.7471 41.0043 9.75001 42.2095H11.1151C11.1151 41.6749 11.447 41.3656 11.9418 41.3656C12.4279 41.3656 12.7801 41.6633 12.7801 42.1517C12.7801 42.5939 12.5006 42.8945 12.0116 43.3309L9.83733 45.2211V46.25Z" fill="#CCCCDC" fill-opacity="0.4"/>
|
||||
</g>
|
||||
<g clip-path="url(#clip1_1899_35965)">
|
||||
<path d="M28 0.800641V10H52V1.05456e-06H29.0029L28.0002 0L28 0.800641Z" fill="#CCCCDC" fill-opacity="0.04"/>
|
||||
<path d="M52 13H28V22.5H52V13Z" fill="#CCCCDC" fill-opacity="0.12"/>
|
||||
<path d="M52 25.75H28V35.25H52V25.75Z" fill="#CCCCDC" fill-opacity="0.12"/>
|
||||
<path d="M52 38.5H28V48H52V38.5Z" fill="#CCCCDC" fill-opacity="0.12"/>
|
||||
<path d="M39.9792 20.875C41.3085 20.875 42.2528 20.1507 42.25 19.1355C42.2528 18.4168 41.7958 17.9035 40.9208 17.798V17.7524C41.5771 17.6554 42.0423 17.202 42.0395 16.5461C42.0423 15.5936 41.206 14.875 39.9903 14.875C38.7663 14.875 37.8746 15.605 37.8635 16.6431H39.1734C39.1845 16.2438 39.5334 15.9758 39.9903 15.9758C40.4251 15.9758 40.7214 16.2467 40.7186 16.6374C40.7214 17.0423 40.3725 17.3218 39.8685 17.3218H39.3174V18.3256H39.8685C40.4417 18.3256 40.8155 18.6164 40.81 19.0271C40.8155 19.4406 40.4721 19.7286 39.9848 19.7286C39.5001 19.7286 39.1429 19.4634 39.1291 19.0784H37.75C37.7611 20.1336 38.6832 20.875 39.9792 20.875Z" fill="#CCCCDC" fill-opacity="0.4"/>
|
||||
<path d="M37.5 32.4453V31.4463L40.0893 27.5H41.7308V31.4287H42.5V32.4453H41.7308V33.5H40.477V32.4453H37.5ZM40.5012 31.4287V28.8828H40.4528L38.8204 31.3818V31.4287H40.5012Z" fill="#CCCCDC" fill-opacity="0.4"/>
|
||||
<path d="M40.2102 46.25C38.8025 46.25 37.7766 45.5188 37.75 44.4957H39.1444C39.1776 44.9552 39.6391 45.2673 40.2102 45.2673C40.8841 45.2673 41.3655 44.8425 41.3655 44.2269C41.3655 43.6055 40.8742 43.1749 40.1902 43.172C39.7885 43.172 39.3835 43.3251 39.1743 43.5708L37.8961 43.3714L38.2181 40.25H42.3748V41.2731H39.4034L39.2274 42.7558H39.2673C39.5063 42.4639 40.0209 42.2471 40.6185 42.2471C41.8436 42.2471 42.7533 43.0621 42.75 44.1951C42.7533 45.3916 41.7241 46.25 40.2102 46.25Z" fill="#CCCCDC" fill-opacity="0.4"/>
|
||||
</g>
|
||||
<path d="M80 0.800641V10H104V1.05456e-06H81.0029L80.0002 0L80 0.800641Z" fill="#CCCCDC" fill-opacity="0.04"/>
|
||||
<path d="M104 17H80V24H104V17Z" fill="#CCCCDC" fill-opacity="0.12"/>
|
||||
<path d="M104 33H80V40H104V33Z" fill="#CCCCDC" fill-opacity="0.12"/>
|
||||
<path d="M104 41H80V48H104V41Z" fill="#CCCCDC" fill-opacity="0.12"/>
|
||||
<path d="M91.8577 23C92.9654 23 93.7523 22.3964 93.75 21.5504C93.7523 20.9515 93.3715 20.5238 92.6423 20.4358V20.3978C93.1892 20.317 93.5769 19.9392 93.5746 19.3926C93.5769 18.5989 92.88 18 91.8669 18C90.8469 18 90.1038 18.6084 90.0946 19.4734H91.1862C91.1954 19.1407 91.4861 18.9173 91.8669 18.9173C92.2292 18.9173 92.4761 19.1431 92.4738 19.4686C92.4761 19.8061 92.1854 20.039 91.7654 20.039H91.3062V20.8755H91.7654C92.2431 20.8755 92.5546 21.1179 92.55 21.4601C92.5546 21.8047 92.2685 22.0447 91.8623 22.0447C91.4585 22.0447 91.1608 21.8237 91.1492 21.5029H90C90.0092 22.3821 90.7777 23 91.8577 23Z" fill="#CCCCDC" fill-opacity="0.4"/>
|
||||
<path d="M90 38.1211V37.2886L92.1578 34H93.5256V37.2739H94.1667V38.1211H93.5256V39H92.4808V38.1211H90ZM92.501 37.2739V35.1523H92.4606L91.1003 37.2349V37.2739H92.501Z" fill="#CCCCDC" fill-opacity="0.4"/>
|
||||
<path d="M92.0501 47C90.877 47 90.0221 46.3907 90 45.5381H91.162C91.1897 45.921 91.5743 46.1811 92.0501 46.1811C92.6118 46.1811 93.0129 45.8271 93.0129 45.3141C93.0129 44.7962 92.6035 44.4374 92.0335 44.435C91.6988 44.435 91.3612 44.5626 91.1869 44.7673L90.1217 44.6012L90.3901 42H93.854V42.8526H91.3778L91.2312 44.0881H91.2644C91.4636 43.8449 91.8924 43.6643 92.3904 43.6643C93.4113 43.6643 94.1694 44.3434 94.1667 45.2876C94.1694 46.2847 93.3117 47 92.0501 47Z" fill="#CCCCDC" fill-opacity="0.4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M104 33H80V47.579H104V33ZM78 28V50H106V28L78 28Z" fill="#CCCCDC" fill-opacity="0.04"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M104 17H80V24H104V17ZM78 12V26H106V12H78Z" fill="#CCCCDC" fill-opacity="0.04"/>
|
||||
<path d="M65.9067 31.75C66.6011 31.75 71.9327 27.75 71.9327 25.75C71.9327 23.75 66.7357 19.75 65.9067 19.75C65.0777 19.75 64.4023 20.25 64.4023 21.2256C64.4023 22.2012 67.9067 24.6706 67.9067 24.6706C67.9067 24.6706 60.2539 24 60 24.6706C59.7461 25.3411 59.7461 26.1589 60 26.8294C60.2539 27.5 67.9067 26.8294 67.9067 26.8294C67.9067 26.8294 64.4023 29.5 64.4023 30.2801C64.4023 31.0603 65.2123 31.75 65.9067 31.75Z" fill="#CCCCDC" fill-opacity="0.04"/>
|
||||
<path d="M81.25 13H80.1787L79 13.6899V14.6304L80.069 14.0151H80.097V17.5H81.25V13Z" fill="#CCCCDC" fill-opacity="0.04"/>
|
||||
<path d="M79.0655 33.5H82.375V32.6351H80.5631V32.6069L81.0979 32.1171C82.0672 31.2782 82.3204 30.8512 82.3204 30.3418C82.3204 29.5419 81.6633 29 80.6482 29C79.6593 29 78.9978 29.5658 79 30.4697H80.0239C80.0239 30.0686 80.2727 29.8367 80.6438 29.8367C81.0084 29.8367 81.2726 30.06 81.2726 30.4263C81.2726 30.7579 81.063 30.9834 80.6962 31.3107L79.0655 32.7283V33.5Z" fill="#CCCCDC" fill-opacity="0.04"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_1899_35965">
|
||||
<rect width="24" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_1899_35965">
|
||||
<rect width="24" height="48" fill="white" transform="translate(28)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 6.2 KiB |
52
public/img/transformations/light/groupToNestedTable.svg
Normal file
52
public/img/transformations/light/groupToNestedTable.svg
Normal file
@ -0,0 +1,52 @@
|
||||
<svg width="106" height="50" viewBox="0 0 106 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1899_521)">
|
||||
<path d="M0 0.800641V10H24V1.05456e-06H1.00292L0.000198677 0L0 0.800641Z" fill="url(#paint0_linear_1899_521)"/>
|
||||
<path d="M24 13H0V22.5H24V13Z" fill="#84AFF1"/>
|
||||
<path d="M24 25.75H0V35.25H24V25.75Z" fill="#84AFF1"/>
|
||||
<path d="M24 38.5H0V48H24V38.5Z" fill="#84AFF1"/>
|
||||
<path d="M13 14.6179H11.5716L10 15.5378V16.7917L11.4253 15.9714H11.4627V20.6179H13V14.6179Z" fill="#24292E"/>
|
||||
<path d="M9.83733 33.5H14.25V32.3468H11.8341V32.3092L12.5472 31.6561C13.8396 30.5376 14.1772 29.9682 14.1772 29.289C14.1772 28.2225 13.3011 27.5 11.9476 27.5C10.629 27.5 9.7471 28.2543 9.75001 29.4595H11.1151C11.1151 28.9249 11.447 28.6156 11.9418 28.6156C12.4279 28.6156 12.7801 28.9133 12.7801 29.4017C12.7801 29.8439 12.5006 30.1445 12.0116 30.5809L9.83733 32.4711V33.5Z" fill="#24292E"/>
|
||||
<path d="M9.83733 46.25H14.25V45.0968H11.8341V45.0592L12.5472 44.4061C13.8396 43.2876 14.1772 42.7182 14.1772 42.039C14.1772 40.9725 13.3011 40.25 11.9476 40.25C10.629 40.25 9.7471 41.0043 9.75001 42.2095H11.1151C11.1151 41.6749 11.447 41.3656 11.9418 41.3656C12.4279 41.3656 12.7801 41.6633 12.7801 42.1517C12.7801 42.5939 12.5006 42.8945 12.0116 43.3309L9.83733 45.2211V46.25Z" fill="#24292E"/>
|
||||
</g>
|
||||
<g clip-path="url(#clip1_1899_521)">
|
||||
<path d="M28 0.800641V10H52V1.05456e-06H29.0029L28.0002 0L28 0.800641Z" fill="url(#paint1_linear_1899_521)"/>
|
||||
<path d="M52 13H28V22.5H52V13Z" fill="#84AFF1"/>
|
||||
<path d="M52 25.75H28V35.25H52V25.75Z" fill="#84AFF1"/>
|
||||
<path d="M52 38.5H28V48H52V38.5Z" fill="#84AFF1"/>
|
||||
<path d="M39.9792 20.875C41.3085 20.875 42.2528 20.1507 42.25 19.1355C42.2528 18.4168 41.7958 17.9035 40.9208 17.798V17.7524C41.5771 17.6554 42.0423 17.202 42.0395 16.5461C42.0423 15.5936 41.206 14.875 39.9903 14.875C38.7663 14.875 37.8746 15.605 37.8635 16.6431H39.1734C39.1845 16.2438 39.5334 15.9758 39.9903 15.9758C40.4251 15.9758 40.7214 16.2467 40.7186 16.6374C40.7214 17.0423 40.3725 17.3218 39.8685 17.3218H39.3174V18.3256H39.8685C40.4417 18.3256 40.8155 18.6164 40.81 19.0271C40.8155 19.4406 40.4721 19.7286 39.9848 19.7286C39.5001 19.7286 39.1429 19.4634 39.1291 19.0784H37.75C37.7611 20.1336 38.6832 20.875 39.9792 20.875Z" fill="#24292E"/>
|
||||
<path d="M37.5 32.4453V31.4463L40.0893 27.5H41.7308V31.4287H42.5V32.4453H41.7308V33.5H40.477V32.4453H37.5ZM40.5012 31.4287V28.8828H40.4528L38.8204 31.3818V31.4287H40.5012Z" fill="#24292E"/>
|
||||
<path d="M40.2102 46.25C38.8025 46.25 37.7766 45.5188 37.75 44.4957H39.1444C39.1776 44.9552 39.6391 45.2673 40.2102 45.2673C40.8841 45.2673 41.3655 44.8425 41.3655 44.2269C41.3655 43.6055 40.8742 43.1749 40.1902 43.172C39.7885 43.172 39.3835 43.3251 39.1743 43.5708L37.8961 43.3714L38.2181 40.25H42.3748V41.2731H39.4034L39.2274 42.7558H39.2673C39.5063 42.4639 40.0209 42.2471 40.6185 42.2471C41.8436 42.2471 42.7533 43.0621 42.75 44.1951C42.7533 45.3916 41.7241 46.25 40.2102 46.25Z" fill="#24292E"/>
|
||||
</g>
|
||||
<path d="M80 0.800641V10H104V1.05456e-06H81.0029L80.0002 0L80 0.800641Z" fill="url(#paint2_linear_1899_521)"/>
|
||||
<path d="M104 17H80V24H104V17Z" fill="#84AFF1"/>
|
||||
<path d="M104 33H80V40H104V33Z" fill="#84AFF1"/>
|
||||
<path d="M104 41H80V48H104V41Z" fill="#84AFF1"/>
|
||||
<path d="M91.8577 23C92.9654 23 93.7523 22.3964 93.75 21.5504C93.7523 20.9515 93.3715 20.5238 92.6423 20.4358V20.3978C93.1892 20.317 93.5769 19.9392 93.5746 19.3926C93.5769 18.5989 92.88 18 91.8669 18C90.8469 18 90.1038 18.6084 90.0946 19.4734H91.1862C91.1954 19.1407 91.4861 18.9173 91.8669 18.9173C92.2292 18.9173 92.4761 19.1431 92.4738 19.4686C92.4761 19.8061 92.1854 20.039 91.7654 20.039H91.3062V20.8755H91.7654C92.2431 20.8755 92.5546 21.1179 92.55 21.4601C92.5546 21.8047 92.2685 22.0447 91.8623 22.0447C91.4585 22.0447 91.1608 21.8237 91.1492 21.5029H90C90.0092 22.3821 90.7777 23 91.8577 23Z" fill="#24292E"/>
|
||||
<path d="M90 38.1211V37.2886L92.1578 34H93.5256V37.2739H94.1667V38.1211H93.5256V39H92.4808V38.1211H90ZM92.501 37.2739V35.1523H92.4606L91.1003 37.2349V37.2739H92.501Z" fill="#24292E"/>
|
||||
<path d="M92.0501 47C90.877 47 90.0221 46.3907 90 45.5381H91.162C91.1897 45.921 91.5743 46.1811 92.0501 46.1811C92.6118 46.1811 93.0129 45.8271 93.0129 45.3141C93.0129 44.7962 92.6035 44.4374 92.0335 44.435C91.6988 44.435 91.3612 44.5626 91.1869 44.7673L90.1217 44.6012L90.3901 42H93.854V42.8526H91.3778L91.2312 44.0881H91.2644C91.4636 43.8449 91.8924 43.6643 92.3904 43.6643C93.4113 43.6643 94.1694 44.3434 94.1667 45.2876C94.1694 46.2847 93.3117 47 92.0501 47Z" fill="#24292E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M104 33H80V47.579H104V33ZM78 28V50H106V28L78 28Z" fill="#24292E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M104 17H80V24H104V17ZM78 12V26H106V12H78Z" fill="#24292E"/>
|
||||
<path d="M65.9067 31.75C66.6011 31.75 71.9327 27.75 71.9327 25.75C71.9327 23.75 66.7357 19.75 65.9067 19.75C65.0777 19.75 64.4023 20.25 64.4023 21.2256C64.4023 22.2012 67.9067 24.6706 67.9067 24.6706C67.9067 24.6706 60.2539 24 60 24.6706C59.7461 25.3411 59.7461 26.1589 60 26.8294C60.2539 27.5 67.9067 26.8294 67.9067 26.8294C67.9067 26.8294 64.4023 29.5 64.4023 30.2801C64.4023 31.0603 65.2123 31.75 65.9067 31.75Z" fill="#24292E"/>
|
||||
<path d="M81.25 13H80.1787L79 13.6899V14.6304L80.069 14.0151H80.097V17.5H81.25V13Z" fill="white"/>
|
||||
<path d="M79.0655 33.5H82.375V32.6351H80.5631V32.6069L81.0979 32.1171C82.0672 31.2782 82.3204 30.8512 82.3204 30.3418C82.3204 29.5419 81.6633 29 80.6482 29C79.6593 29 78.9978 29.5658 79 30.4697H80.0239C80.0239 30.0686 80.2727 29.8367 80.6438 29.8367C81.0084 29.8367 81.2726 30.06 81.2726 30.4263C81.2726 30.7579 81.063 30.9834 80.6962 31.3107L79.0655 32.7283V33.5Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1899_521" x1="0.00781255" y1="5.004" x2="24.0078" y2="5.004" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1899_521" x1="28.0078" y1="5.004" x2="52.0078" y2="5.004" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1899_521" x1="80.0078" y1="5.004" x2="104.008" y2="5.004" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1899_521">
|
||||
<rect width="24" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_1899_521">
|
||||
<rect width="24" height="48" fill="white" transform="translate(28)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 6.3 KiB |
Loading…
Reference in New Issue
Block a user