Commit 1f717f51 by Marcus Andersson Committed by GitHub

TabelPanel: add support for organizing fields/columns. (#23135)

* Added draft on transformers to sort and hide fields.

* added structure for the UI.

* draft on sorting/filtering UI.

* simplified the datastructure a bit.

* added draft on drag and drop support.

* added some super simple styling. Nothing final still waiting for a proper design on this.

* updated lockfile after merge.

* changed so we use the new path for button.

* added one more test.

* Ignore feature toggle

* Moved editor to app

* Added top description

* Minor update

* Did some renaming and simplified the code a bit.

* fixed so we dont use capital naming on the transformer.

* changed to an vertical drag and drop design.

* added support to rename fields.

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
parent 13975335
......@@ -7,7 +7,10 @@ import { filterFieldsByNameTransformer, FilterFieldsByNameTransformerOptions } f
import { noopTransformer } from './transformers/noop';
import { DataTransformerConfig, DataTransformerInfo } from '../types/transformations';
import { filterFramesByRefIdTransformer } from './transformers/filterByRefId';
import { orderFieldsTransformer } from './transformers/order';
import { organizeFieldsTransformer } from './transformers/organize';
import { seriesToColumnsTransformer } from './transformers/seriesToColumns';
import { renameFieldsTransformer } from './transformers/rename';
// Initalize the Registry
......@@ -67,9 +70,12 @@ export const transformersRegistry = new TransformerRegistry(() => [
filterFieldsByNameTransformer,
filterFramesTransformer,
filterFramesByRefIdTransformer,
orderFieldsTransformer,
organizeFieldsTransformer,
appendTransformer,
reduceTransformer,
seriesToColumnsTransformer,
renameFieldsTransformer,
]);
export { ReduceTransformerOptions, FilterFieldsByNameTransformerOptions };
......@@ -3,6 +3,9 @@ export enum DataTransformerID {
append = 'append', // Merge all series together
// rotate = 'rotate', // Columns to rows
reduce = 'reduce', // Run calculations on fields
order = 'order', // order fields based on user configuration
organize = 'organize', // order, rename and filter based on user configuration
rename = 'rename', // rename field based on user configuration
seriesToColumns = 'seriesToColumns', // former table transform timeseries_to_columns
filterFields = 'filterFields', // Pick some fields (keep all frames)
......
import {
ArrayVector,
DataTransformerConfig,
DataTransformerID,
FieldType,
toDataFrame,
transformDataFrame,
} from '@grafana/data';
import { OrderFieldsTransformerOptions } from './order';
describe('Order Transformer', () => {
describe('when consistent data is received', () => {
const data = toDataFrame({
name: 'A',
fields: [
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
{ name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
],
});
it('should order according to config', () => {
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
id: DataTransformerID.order,
options: {
indexByName: {
time: 2,
temperature: 0,
humidity: 1,
},
},
};
const ordered = transformDataFrame([cfg], [data])[0];
expect(ordered.fields).toEqual([
{
config: {},
name: 'temperature',
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
{
config: {},
name: 'humidity',
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
},
{
config: {},
name: 'time',
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]),
},
]);
});
});
describe('when inconsistent data is received', () => {
const data = toDataFrame({
name: 'A',
fields: [
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
{ name: 'pressure', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
],
});
it('should append fields missing in config at the end', () => {
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
id: DataTransformerID.order,
options: {
indexByName: {
time: 2,
temperature: 0,
humidity: 1,
},
},
};
const ordered = transformDataFrame([cfg], [data])[0];
expect(ordered.fields).toEqual([
{
config: {},
name: 'humidity',
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
},
{
config: {},
name: 'time',
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]),
},
{
config: {},
name: 'pressure',
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
]);
});
});
describe('when transforming with empty configuration', () => {
const data = toDataFrame({
name: 'A',
fields: [
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
{ name: 'pressure', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
],
});
it('should keep the same order as in the incoming data', () => {
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
id: DataTransformerID.order,
options: {
indexByName: {},
},
};
const ordered = transformDataFrame([cfg], [data])[0];
expect(ordered.fields).toEqual([
{
config: {},
name: 'time',
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]),
},
{
config: {},
name: 'pressure',
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
{
config: {},
name: 'humidity',
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
},
]);
});
});
});
import { DataTransformerID } from './ids';
import { DataTransformerInfo } from '../../types/transformations';
import { DataFrame } from '../..';
import { Field } from '../../types';
export interface OrderFieldsTransformerOptions {
indexByName: Record<string, number>;
}
export const orderFieldsTransformer: DataTransformerInfo<OrderFieldsTransformerOptions> = {
id: DataTransformerID.order,
name: 'Order fields by name',
description: 'Order fields based on configuration given by user',
defaultOptions: {
indexByName: {},
},
/**
* Return a modified copy of the series. If the transform is not or should not
* be applied, just return the input series
*/
transformer: (options: OrderFieldsTransformerOptions) => {
const orderer = createFieldsOrderer(options.indexByName);
return (data: DataFrame[]) => {
if (!Array.isArray(data) || data.length === 0) {
return data;
}
return data.map(frame => ({
...frame,
fields: orderer(frame.fields),
}));
};
},
};
export const createFieldsComparer = (indexByName: Record<string, number>) => (a: string, b: string) => {
return indexOfField(a, indexByName) - indexOfField(b, indexByName);
};
const createFieldsOrderer = (indexByName: Record<string, number>) => (fields: Field[]) => {
if (!Array.isArray(fields) || fields.length === 0) {
return fields;
}
if (!indexByName || Object.keys(indexByName).length === 0) {
return fields;
}
const comparer = createFieldsComparer(indexByName);
return fields.sort((a, b) => comparer(a.name, b.name));
};
const indexOfField = (fieldName: string, indexByName: Record<string, number>) => {
if (Number.isInteger(indexByName[fieldName])) {
return indexByName[fieldName];
}
return Number.MAX_SAFE_INTEGER;
};
import {
ArrayVector,
DataTransformerConfig,
DataTransformerID,
FieldType,
toDataFrame,
transformDataFrame,
} from '@grafana/data';
import { OrganizeFieldsTransformerOptions } from './organize';
describe('OrganizeFields Transformer', () => {
describe('when consistent data is received', () => {
const data = toDataFrame({
name: 'A',
fields: [
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
{ name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
],
});
it('should order and filter according to config', () => {
const cfg: DataTransformerConfig<OrganizeFieldsTransformerOptions> = {
id: DataTransformerID.organize,
options: {
indexByName: {
time: 2,
temperature: 0,
humidity: 1,
},
excludeByName: {
time: true,
},
renameByName: {
humidity: 'renamed_humidity',
},
},
};
const organized = transformDataFrame([cfg], [data])[0];
expect(organized.fields).toEqual([
{
config: {},
name: 'temperature',
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
{
config: {},
name: 'renamed_humidity',
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
},
]);
});
});
describe('when inconsistent data is received', () => {
const data = toDataFrame({
name: 'A',
fields: [
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
{ name: 'pressure', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
],
});
it('should append fields missing in config at the end', () => {
const cfg: DataTransformerConfig<OrganizeFieldsTransformerOptions> = {
id: DataTransformerID.organize,
options: {
indexByName: {
time: 2,
temperature: 0,
humidity: 1,
},
excludeByName: {
humidity: true,
},
renameByName: {
time: 'renamed_time',
},
},
};
const organized = transformDataFrame([cfg], [data])[0];
expect(organized.fields).toEqual([
{
config: {},
name: 'renamed_time',
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]),
},
{
config: {},
name: 'pressure',
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
]);
});
});
});
import { DataTransformerID } from './ids';
import { DataTransformerInfo } from '../../types/transformations';
import { OrderFieldsTransformerOptions, orderFieldsTransformer } from './order';
import { filterFieldsByNameTransformer } from './filterByName';
import { DataFrame } from '../..';
import { RenameFieldsTransformerOptions, renameFieldsTransformer } from './rename';
export interface OrganizeFieldsTransformerOptions
extends OrderFieldsTransformerOptions,
RenameFieldsTransformerOptions {
excludeByName: Record<string, boolean>;
}
export const organizeFieldsTransformer: DataTransformerInfo<OrganizeFieldsTransformerOptions> = {
id: DataTransformerID.organize,
name: 'Organize fields by name',
description: 'Order, filter and rename fields based on configuration given by user',
defaultOptions: {
excludeByName: {},
indexByName: {},
renameByName: {},
},
/**
* Return a modified copy of the series. If the transform is not or should not
* be applied, just return the input series
*/
transformer: (options: OrganizeFieldsTransformerOptions) => {
const rename = renameFieldsTransformer.transformer(options);
const order = orderFieldsTransformer.transformer(options);
const filter = filterFieldsByNameTransformer.transformer({
exclude: mapToExcludeRegexp(options.excludeByName),
});
return (data: DataFrame[]) => rename(order(filter(data)));
},
};
const mapToExcludeRegexp = (excludeByName: Record<string, boolean>): string | undefined => {
if (!excludeByName) {
return undefined;
}
const fieldsToExclude = Object.keys(excludeByName)
.filter(name => excludeByName[name])
.join('|');
if (fieldsToExclude.length === 0) {
return undefined;
}
return `^(${fieldsToExclude})$`;
};
import {
ArrayVector,
DataTransformerConfig,
DataTransformerID,
FieldType,
toDataFrame,
transformDataFrame,
} from '@grafana/data';
import { RenameFieldsTransformerOptions } from './rename';
describe('Rename Transformer', () => {
describe('when consistent data is received', () => {
const data = toDataFrame({
name: 'A',
fields: [
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
{ name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
],
});
it('should rename according to config', () => {
const cfg: DataTransformerConfig<RenameFieldsTransformerOptions> = {
id: DataTransformerID.rename,
options: {
renameByName: {
time: 'Total time',
humidity: 'Moistiness',
temperature: 'how cold is it?',
},
},
};
const renamed = transformDataFrame([cfg], [data])[0];
expect(renamed.fields).toEqual([
{
config: {},
name: 'Total time',
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]),
},
{
config: {},
name: 'how cold is it?',
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
{
config: {},
name: 'Moistiness',
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
},
]);
});
});
describe('when inconsistent data is received', () => {
const data = toDataFrame({
name: 'A',
fields: [
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
{ name: 'pressure', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
],
});
it('should not rename fields missing in config', () => {
const cfg: DataTransformerConfig<RenameFieldsTransformerOptions> = {
id: DataTransformerID.rename,
options: {
renameByName: {
time: 'ttl',
temperature: 'temp',
humidity: 'hum',
},
},
};
const renamed = transformDataFrame([cfg], [data])[0];
expect(renamed.fields).toEqual([
{
config: {},
name: 'ttl',
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]),
},
{
config: {},
name: 'pressure',
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
{
config: {},
name: 'hum',
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
},
]);
});
});
describe('when transforming with empty configuration', () => {
const data = toDataFrame({
name: 'A',
fields: [
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
{ name: 'pressure', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
],
});
it('should keep the same names as in the incoming data', () => {
const cfg: DataTransformerConfig<RenameFieldsTransformerOptions> = {
id: DataTransformerID.rename,
options: {
renameByName: {},
},
};
const renamed = transformDataFrame([cfg], [data])[0];
expect(renamed.fields).toEqual([
{
config: {},
name: 'time',
type: FieldType.time,
values: new ArrayVector([3000, 4000, 5000, 6000]),
},
{
config: {},
name: 'pressure',
type: FieldType.number,
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
},
{
config: {},
name: 'humidity',
type: FieldType.number,
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
},
]);
});
});
});
import { DataTransformerID } from './ids';
import { DataTransformerInfo } from '../../types/transformations';
import { DataFrame, Field } from '../..';
export interface RenameFieldsTransformerOptions {
renameByName: Record<string, string>;
}
export const renameFieldsTransformer: DataTransformerInfo<RenameFieldsTransformerOptions> = {
id: DataTransformerID.rename,
name: 'Rename fields by name',
description: 'Rename fields based on configuration given by user',
defaultOptions: {
renameByName: {},
},
/**
* Return a modified copy of the series. If the transform is not or should not
* be applied, just return the input series
*/
transformer: (options: RenameFieldsTransformerOptions) => {
const renamer = createRenamer(options.renameByName);
return (data: DataFrame[]) => {
if (!Array.isArray(data) || data.length === 0) {
return data;
}
return data.map(frame => ({
...frame,
fields: renamer(frame.fields),
}));
};
},
};
const createRenamer = (renameByName: Record<string, string>) => (fields: Field[]): Field[] => {
if (!renameByName || Object.keys(renameByName).length === 0) {
return fields;
}
return fields.map(field => {
const renameTo = renameByName[field.name];
if (typeof renameTo !== 'string' || renameTo.length === 0) {
return field;
}
return {
...field,
name: renameTo,
};
});
};
......@@ -32,6 +32,7 @@
"@grafana/slate-react": "0.22.9-grafana",
"@grafana/tsconfig": "^1.0.0-rc1",
"@torkelo/react-select": "3.0.8",
"@types/react-beautiful-dnd": "12.1.2",
"@types/react-color": "3.0.1",
"@types/react-select": "3.0.8",
"@types/react-table": "7.0.12",
......@@ -51,6 +52,7 @@
"rc-slider": "9.2.3",
"rc-time-picker": "^3.7.3",
"react": "16.12.0",
"react-beautiful-dnd": "13.0.0",
"react-calendar": "2.19.2",
"react-color": "2.18.0",
"react-custom-scrollbars": "4.2.1",
......
......@@ -12,7 +12,7 @@ type Justify = 'flex-start' | 'flex-end' | 'space-between' | 'center';
type Align = 'normal' | 'flex-start' | 'flex-end' | 'center';
export interface LayoutProps {
children: React.ReactNode[];
children: React.ReactNode[] | React.ReactNode;
orientation?: Orientation;
spacing?: Spacing;
justify?: Justify;
......
import React, { useMemo, useCallback } from 'react';
import { css, cx } from 'emotion';
import { OrganizeFieldsTransformerOptions } from '@grafana/data/src/transformations/transformers/organize';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
import { TransformerUIRegistyItem, TransformerUIProps } from './types';
import { DataTransformerID, transformersRegistry, DataFrame, GrafanaTheme } from '@grafana/data';
import { stylesFactory, useTheme } from '../../themes';
import { Button } from '../Button';
import { createFieldsComparer } from '@grafana/data/src/transformations/transformers/order';
import { VerticalGroup } from '../Layout/Layout';
import { Input } from '../Forms/Input/Input';
interface OrganizeFieldsTransformerEditorProps extends TransformerUIProps<OrganizeFieldsTransformerOptions> {}
const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorProps> = props => {
const { options, input, onChange } = props;
const { indexByName, excludeByName, renameByName } = options;
const fieldNames = useMemo(() => fieldNamesFromInput(input), [input]);
const orderedFieldNames = useMemo(() => orderFieldNamesByIndex(fieldNames, indexByName), [fieldNames, indexByName]);
const onToggleVisibility = useCallback(
(field: string, shouldExclude: boolean) => {
onChange({
...options,
excludeByName: {
...excludeByName,
[field]: shouldExclude,
},
});
},
[onChange, excludeByName, indexByName]
);
const onDragEnd = useCallback(
(result: DropResult) => {
if (!result || !result.destination) {
return;
}
const startIndex = result.source.index;
const endIndex = result.destination.index;
if (startIndex === endIndex) {
return;
}
onChange({
...options,
indexByName: reorderToIndex(fieldNames, startIndex, endIndex),
});
},
[onChange, indexByName, excludeByName, fieldNames]
);
const onRenameField = useCallback(
(from: string, to: string) => {
onChange({
...options,
renameByName: {
...options.renameByName,
[from]: to,
},
});
},
[onChange, fieldNames, renameByName]
);
return (
<VerticalGroup>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="sortable-fields-transformer" direction="vertical">
{provided => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{orderedFieldNames.map((fieldName, index) => {
return (
<DraggableFieldName
fieldName={fieldName}
index={index}
onToggleVisibility={onToggleVisibility}
onRenameField={onRenameField}
visible={!excludeByName[fieldName]}
key={fieldName}
/>
);
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</VerticalGroup>
);
};
interface DraggableFieldProps {
fieldName: string;
index: number;
visible: boolean;
onToggleVisibility: (fieldName: string, isVisible: boolean) => void;
onRenameField: (from: string, to: string) => void;
}
const DraggableFieldName: React.FC<DraggableFieldProps> = ({
fieldName,
index,
visible,
onToggleVisibility,
onRenameField,
}) => {
const theme = useTheme();
const styles = getFieldNameStyles(theme);
return (
<Draggable draggableId={fieldName} index={index}>
{provided => (
<div
className={styles.container}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className={styles.left}>
<i className={cx('fa fa-ellipsis-v', styles.draggable)} />
<Button
className={styles.toggle}
variant="link"
size="md"
icon={visible ? 'fa fa-eye' : 'fa fa-eye-slash'}
onClick={() => onToggleVisibility(fieldName, visible)}
/>
<span className={styles.name}>{fieldName}</span>
</div>
<div className={styles.right}>
<Input
placeholder={`Rename ${fieldName}`}
onChange={event => onRenameField(fieldName, event.currentTarget.value)}
/>
</div>
</div>
)}
</Draggable>
);
};
const getFieldNameStyles = stylesFactory((theme: GrafanaTheme) => ({
container: css`
display: flex;
align-items: center;
margin-top: 8px;
`,
left: css`
width: 35%;
padding: 0 8px;
border-radius: 3px;
background-color: ${theme.isDark ? theme.colors.grayBlue : theme.colors.gray6};
border: 1px solid ${theme.isDark ? theme.colors.dark6 : theme.colors.gray5};
`,
right: css`
width: 65%;
margin-left: 8px;
`,
toggle: css`
padding: 5px;
margin: 0 5px;
`,
draggable: css`
font-size: ${theme.typography.size.md};
opacity: 0.4;
`,
name: css`
font-size: ${theme.typography.size.sm};
font-weight: ${theme.typography.weight.semibold};
`,
}));
const reorderToIndex = (fieldNames: string[], startIndex: number, endIndex: number) => {
const result = Array.from(fieldNames);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result.reduce((nameByIndex, fieldName, index) => {
nameByIndex[fieldName] = index;
return nameByIndex;
}, {} as Record<string, number>);
};
const orderFieldNamesByIndex = (fieldNames: string[], indexByName: Record<string, number> = {}): string[] => {
if (!indexByName || Object.keys(indexByName).length === 0) {
return fieldNames;
}
const comparer = createFieldsComparer(indexByName);
return fieldNames.sort(comparer);
};
const fieldNamesFromInput = (input: DataFrame[]): string[] => {
if (!Array.isArray(input)) {
return [] as string[];
}
return Object.keys(
input.reduce((names, frame) => {
if (!frame || !Array.isArray(frame.fields)) {
return names;
}
return frame.fields.reduce((names, field) => {
names[field.name] = null;
return names;
}, names);
}, {} as Record<string, null>)
);
};
export const organizeFieldsTransformRegistryItem: TransformerUIRegistyItem<OrganizeFieldsTransformerOptions> = {
id: DataTransformerID.organize,
component: OrganizeFieldsTransformerEditor,
transformer: transformersRegistry.get(DataTransformerID.organize),
name: 'Organize fields',
description: 'UI for organizing fields',
};
......@@ -3,11 +3,13 @@ import { reduceTransformRegistryItem } from './ReduceTransformerEditor';
import { filterFieldsByNameTransformRegistryItem } from './FilterByNameTransformerEditor';
import { filterFramesByRefIdTransformRegistryItem } from './FilterByRefIdTransformerEditor';
import { TransformerUIRegistyItem } from './types';
import { organizeFieldsTransformRegistryItem } from './OrganizeFieldsTransformerEditor';
export const transformersUIRegistry = new Registry<TransformerUIRegistyItem<any>>(() => {
return [
reduceTransformRegistryItem,
filterFieldsByNameTransformRegistryItem,
filterFramesByRefIdTransformRegistryItem,
organizeFieldsTransformRegistryItem,
];
});
......@@ -103,8 +103,6 @@ export { DataLinkInput } from './DataLinks/DataLinkInput';
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
export { SeriesIcon } from './Legend/SeriesIcon';
export { transformersUIRegistry } from './TransformersUI/transformers';
export { TransformationRow } from './TransformersUI/TransformationRow';
export { TransformationsEditor } from './TransformersUI/TransformationsEditor';
export { JSONFormatter } from './JSONFormatter/JSONFormatter';
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary';
......
import React from 'react';
import { config } from 'app/core/config';
import { css } from 'emotion';
import { TabsBar, Tab, stylesFactory, TabContent, TransformationsEditor } from '@grafana/ui';
import { TabsBar, Tab, stylesFactory, TabContent } from '@grafana/ui';
import { DataTransformerConfig, LoadingState, PanelData } from '@grafana/data';
import { PanelEditorTab, PanelEditorTabId } from './types';
import { DashboardModel } from '../../state';
......@@ -9,6 +9,7 @@ import { QueriesTab } from '../../panel_editor/QueriesTab';
import { PanelModel } from '../../state/PanelModel';
import { AlertTab } from 'app/features/alerting/AlertTab';
import { VisualizationTab } from './VisualizationTab';
import { TransformationsEditor } from '../TransformationsEditor/TransformationsEditor';
interface PanelEditorTabsProps {
panel: PanelModel;
......
......@@ -17,7 +17,7 @@ export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardMod
const panel = dashboard.initPanelEditor(sourcePanel);
const queryRunner = panel.getQueryRunner();
const querySubscription = queryRunner.getData().subscribe({
const querySubscription = queryRunner.getData(false).subscribe({
next: (data: PanelData) => dispatch(setEditorPanelData(data)),
});
......
import React, { useContext, useState } from 'react';
import { ThemeContext } from '../../themes/ThemeContext';
import { css } from 'emotion';
import { DataFrame } from '@grafana/data';
import { JSONFormatter } from '../JSONFormatter/JSONFormatter';
import { GrafanaTheme } from '@grafana/data';
import { JSONFormatter, ThemeContext } from '@grafana/ui';
import { GrafanaTheme, DataFrame } from '@grafana/data';
interface TransformationRowProps {
name: string;
......@@ -13,6 +11,39 @@ interface TransformationRowProps {
input: DataFrame[];
}
export const TransformationRow = ({ onRemove, editor, name, input }: TransformationRowProps) => {
const theme = useContext(ThemeContext);
const [viewDebug, setViewDebug] = useState(false);
const styles = getStyles(theme);
return (
<div
className={css`
margin-bottom: 10px;
`}
>
<div className={styles.title}>
<div className={styles.name}>{name}</div>
<div className={styles.iconRow}>
<div onClick={() => setViewDebug(!viewDebug)} className={styles.icon}>
<i className="fa fa-fw fa-bug" />
</div>
<div onClick={onRemove} className={styles.icon}>
<i className="fa fa-fw fa-trash" />
</div>
</div>
</div>
<div className={styles.editor}>
{editor}
{viewDebug && (
<div>
<JSONFormatter json={input} />
</div>
)}
</div>
</div>
);
};
const getStyles = (theme: GrafanaTheme) => ({
title: css`
display: flex;
......@@ -50,36 +81,3 @@ const getStyles = (theme: GrafanaTheme) => ({
padding: 8px;
`,
});
export const TransformationRow = ({ onRemove, editor, name, input }: TransformationRowProps) => {
const theme = useContext(ThemeContext);
const [viewDebug, setViewDebug] = useState(false);
const styles = getStyles(theme);
return (
<div
className={css`
margin-bottom: 10px;
`}
>
<div className={styles.title}>
<div className={styles.name}>{name}</div>
<div className={styles.iconRow}>
<div onClick={() => setViewDebug(!viewDebug)} className={styles.icon}>
<i className="fa fa-fw fa-bug" />
</div>
<div onClick={onRemove} className={styles.icon}>
<i className="fa fa-fw fa-trash" />
</div>
</div>
</div>
<div className={styles.editor}>
{editor}
{viewDebug && (
<div>
<JSONFormatter json={input} />
</div>
)}
</div>
</div>
);
};
import { DataTransformerID, DataTransformerConfig, DataFrame, transformDataFrame } from '@grafana/data';
import { Select } from '../Forms/Legacy/Select/Select';
import { transformersUIRegistry } from './transformers';
import { css } from 'emotion';
import React from 'react';
import { transformersUIRegistry } from '@grafana/ui/src/components/TransformersUI/transformers';
import { DataTransformerID, DataTransformerConfig, DataFrame, transformDataFrame } from '@grafana/data';
import { Button, Select } from '@grafana/ui';
import { TransformationRow } from './TransformationRow';
import { Button } from '../Button';
import { css } from 'emotion';
interface TransformationsEditorState {
updateCounter: number;
}
interface TransformationsEditorProps {
interface Props {
onChange: (transformations: DataTransformerConfig[]) => void;
transformations: DataTransformerConfig[];
dataFrames: DataFrame[];
}
export class TransformationsEditor extends React.PureComponent<TransformationsEditorProps, TransformationsEditorState> {
interface State {
updateCounter: number;
}
export class TransformationsEditor extends React.PureComponent<Props, State> {
state = { updateCounter: 0 };
onTransformationAdd = () => {
......@@ -116,12 +115,16 @@ export class TransformationsEditor extends React.PureComponent<TransformationsEd
render() {
return (
<>
<div className="panel-editor__content">
<p className="muted text-center" style={{ padding: '8px' }}>
Transformations allow you to combine, re-order, hide and rename specific parts the the data set before being
visualized.
</p>
{this.renderTransformationEditors()}
<Button variant="secondary" icon="fa fa-plus" onClick={this.onTransformationAdd}>
Add transformation
</Button>
</>
</div>
);
}
}
......@@ -4,7 +4,6 @@ import { ReplaySubject, Unsubscribable, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
// Services & Utils
import { config } from 'app/core/config';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import kbn from 'app/core/utils/kbn';
import templateSrv from 'app/features/templating/template_srv';
......@@ -70,37 +69,38 @@ export class PanelQueryRunner {
return this.subject.pipe(
map((data: PanelData) => {
let processedData = data;
// apply transformations
if (transform && this.hasTransformations()) {
processedData = {
...processedData,
series: transformDataFrame(this.dataConfigSource.getTransformations(), data.series),
};
// Apply transformations
if (transform) {
const transformations = this.dataConfigSource.getTransformations();
if (transformations && transformations.length > 0) {
processedData = {
...processedData,
series: transformDataFrame(this.dataConfigSource.getTransformations(), data.series),
};
}
}
// apply overrides
if (this.hasFieldOverrideOptions()) {
// Apply field defaults & overrides
const fieldConfig = this.dataConfigSource.getFieldOverrideOptions();
if (fieldConfig) {
processedData = {
...processedData,
series: applyFieldOverrides({
data: processedData.series,
...this.dataConfigSource.getFieldOverrideOptions(),
...fieldConfig,
}),
};
}
return processedData;
})
);
}
hasTransformations = () => {
const transformations = this.dataConfigSource.getTransformations();
return config.featureToggles.transformations && transformations && transformations.length > 0;
};
hasFieldOverrideOptions = () => {
return this.dataConfigSource.getFieldOverrideOptions();
};
async run(options: QueryRunnerOptions) {
const {
queries,
......
......@@ -70,7 +70,7 @@
}
.panel-editor__content {
padding: 15px;
padding: 16px;
}
.panel-in-fullscreen {
......
......@@ -5794,6 +5794,13 @@
"@types/history" "*"
"@types/react" "*"
"@types/react-beautiful-dnd@12.1.2":
version "12.1.2"
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-12.1.2.tgz#dfd1bdb072e92c1363e5f7a4c1842eaf95f77b21"
integrity sha512-h+0mA4cHmzL4BhyCniB6ZSSZhfO9LpXXbnhdAfa2k7klS03woiOT+Dh5AchY6eoQXk3vQVtqn40YY3u+MwFs8A==
dependencies:
"@types/react" "*"
"@types/react-color@3.0.1", "@types/react-color@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.1.tgz#5433e2f503ea0e0831cbc6fd0c20f8157d93add0"
......@@ -8138,16 +8145,6 @@ browserslist@^4.8.5:
electron-to-chromium "^1.3.341"
node-releases "^1.1.47"
browserslist@^4.9.1:
version "4.11.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.0.tgz#aef4357b10a8abda00f97aac7cd587b2082ba1ad"
integrity sha512-WqEC7Yr5wUH5sg6ruR++v2SGOQYpyUdYYd4tZoAq1F7y+QXoLoYGXVbxhtaIqWmAJjtNTRjVD3HuJc1OXTel2A==
dependencies:
caniuse-lite "^1.0.30001035"
electron-to-chromium "^1.3.380"
node-releases "^1.1.52"
pkg-up "^3.1.0"
bs-logger@0.x:
version "0.2.6"
resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
......@@ -8445,6 +8442,11 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-db@1.0.30000772:
version "1.0.30000772"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b"
integrity sha1-UarokXaChureSj2DGep21qAbUSs=
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30000999:
version "1.0.30000999"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000999.tgz#427253a69ad7bea4aa8d8345687b8eec51ca0e43"
......@@ -8465,11 +8467,6 @@ caniuse-lite@^1.0.30001023:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001027.tgz#283e2ef17d94889cc216a22c6f85303d78ca852d"
integrity sha512-7xvKeErvXZFtUItTHgNtLgS9RJpVnwBlWX8jSo/BO8VsF6deszemZSkJJJA1KOKrXuzZH4WALpAJdq5EyfgMLg==
caniuse-lite@^1.0.30001035:
version "1.0.30001036"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001036.tgz#930ea5272010d8bf190d859159d757c0b398caf0"
integrity sha512-jU8CIFIj2oR7r4W+5AKcsvWNVIb6Q6OZE3UsrXrZBHFtreT4YgTeOJtTucp+zSedEpTi3L5wASSP0LYIE3if6w==
capture-exit@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
......@@ -9702,6 +9699,13 @@ css-blank-pseudo@^0.1.4:
dependencies:
postcss "^7.0.5"
css-box-model@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.0.tgz#3a26377b4162b3200d2ede4b064ec5b6a75186d0"
integrity sha512-lri0br+jSNV0kkkiGEp9y9y3Njq2PmpqbeGWRFQJuZteZzY9iC9GZhQ8Y4WpPwM/2YocjHePxy14igJY7YKzkA==
dependencies:
tiny-invariant "^1.0.6"
css-color-names@0.0.4, css-color-names@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
......@@ -10930,6 +10934,11 @@ electron-to-chromium@^1.3.341:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.349.tgz#663f26a69d348a462df47b4d7ab162a2f29bbcb7"
integrity sha512-uEb2zs6EJ6OZIqaMsCSliYVgzE/f7/s1fLWqtvRtHg/v5KBF2xds974fUnyatfxIDgkqzQVwFtam5KExqywx0Q==
electron-to-chromium@^1.3.378:
version "1.3.395"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.395.tgz#2c531a0477bcc41deb440877d1f27509ed286aed"
integrity sha512-kdn2cX6hZXDdz/O2Q8tZscITlsSv1a/7bOq/fQs7QAJ9iaRlnhZPccarNhxZv1tXgmgwCnKp/1lJNYLOG8Dxiw==
electron-to-chromium@^1.3.380:
version "1.3.381"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.381.tgz#952678ff91a5f36175a3832358a6dd2de3bf62b7"
......@@ -16686,7 +16695,7 @@ mem@^4.0.0:
mimic-fn "^2.0.0"
p-is-promise "^2.0.0"
memoize-one@5.1.1, memoize-one@^5.0.0:
memoize-one@5.1.1, "memoize-one@>=3.1.1 <6", memoize-one@^5.0.0, memoize-one@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
......@@ -20080,6 +20089,11 @@ quick-lru@^1.0.0:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=
raf-schd@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
raf@^3.1.0, raf@^3.4.0, raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
......@@ -20318,6 +20332,19 @@ react-addons-create-fragment@^15.6.2:
loose-envify "^1.3.1"
object-assign "^4.1.0"
react-beautiful-dnd@13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40"
integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==
dependencies:
"@babel/runtime" "^7.8.4"
css-box-model "^1.2.0"
memoize-one "^5.1.1"
raf-schd "^4.0.2"
react-redux "^7.1.1"
redux "^4.0.4"
use-memo-one "^1.1.1"
react-calendar@2.19.2:
version "2.19.2"
resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-2.19.2.tgz#496e78eb11a00aee1ae6b5d02d221ed1ca2db952"
......@@ -20694,6 +20721,17 @@ react-redux@7.1.1:
prop-types "^15.7.2"
react-is "^16.9.0"
react-redux@^7.1.1:
version "7.2.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
dependencies:
"@babel/runtime" "^7.5.5"
hoist-non-react-statics "^3.3.0"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.9.0"
react-resizable@^1.9.0:
version "1.10.1"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.10.1.tgz#f0c2cf1d83b3470b87676ce6d6b02bbe3f4d8cd4"
......@@ -21207,6 +21245,14 @@ redux@^3.6.0:
loose-envify "^1.1.0"
symbol-observable "^1.0.3"
redux@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
reflect.ownkeys@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
......@@ -23920,6 +23966,11 @@ tiny-invariant@^1.0.1:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73"
integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==
tiny-invariant@^1.0.6:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-0.0.3.tgz#1807eb4c5f81784a6354d58ea1d5024f18c6c81f"
......@@ -24718,6 +24769,11 @@ use-callback-ref@^1.2.1:
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.1.tgz#898759ccb9e14be6c7a860abafa3ffbd826c89bb"
integrity sha512-C3nvxh0ZpaOxs9RCnWwAJ+7bJPwQI8LHF71LzbQ3BvzH5XkdtlkMadqElGevg5bYBDFip4sAnD4m06zAKebg1w==
use-memo-one@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c"
integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==
use-sidecar@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment