Transformations: Improve UI and add some love to filter by name (#23751)

* Change filterByName options to accept arrays instead of strings

* Improve transformations UI

* Minor updates

* Minor UI changes

* Review
This commit is contained in:
Dominik Prokop 2020-04-22 13:38:50 +02:00 committed by GitHub
parent bcf5d4b25c
commit 6715cf22a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 217 additions and 227 deletions

View File

@ -35,7 +35,7 @@ describe('filterByName transformer', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
include: '/^(startsWith)/',
include: ['^(startsWith)'],
},
};
@ -48,7 +48,7 @@ describe('filterByName transformer', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
exclude: '/^(startsWith)/',
exclude: ['^(startsWith)'],
},
};
@ -61,8 +61,8 @@ describe('filterByName transformer', () => {
const cfg = {
id: DataTransformerID.filterFieldsByName,
options: {
exclude: '/^(startsWith)/',
include: `/^(B)$/`,
exclude: ['^(startsWith)'],
include: [`^(B)$`],
},
};

View File

@ -4,8 +4,8 @@ import { DataTransformerInfo } from '../../types/transformations';
import { FieldMatcherID } from '../matchers/ids';
export interface FilterFieldsByNameTransformerOptions {
include?: string;
exclude?: string;
include?: string[];
exclude?: string[];
}
export const filterFieldsByNameTransformer: DataTransformerInfo<FilterFieldsByNameTransformerOptions> = {
@ -23,16 +23,21 @@ export const filterFieldsByNameTransformer: DataTransformerInfo<FilterFieldsByNa
if (options.include) {
filterOptions.include = {
id: FieldMatcherID.byName,
options: options.include,
options: options.include.length > 0 ? buildRegex(options.include) : '',
};
}
if (options.exclude) {
filterOptions.exclude = {
id: FieldMatcherID.byName,
options: options.exclude,
options: options.exclude.length > 0 ? buildRegex(options.exclude) : '',
};
}
return filterFieldsTransformer.transformer(filterOptions);
},
};
const buildRegex = (regexs: string[]) => {
const include = regexs.map(s => `(${s})`).join('|');
return `/${include}/`;
};

View File

@ -29,25 +29,17 @@ export const organizeFieldsTransformer: DataTransformerInfo<OrganizeFieldsTransf
const rename = renameFieldsTransformer.transformer(options);
const order = orderFieldsTransformer.transformer(options);
const filter = filterFieldsByNameTransformer.transformer({
exclude: mapToExcludeRegexp(options.excludeByName),
exclude: mapToExcludeArray(options.excludeByName),
});
return (data: DataFrame[]) => rename(order(filter(data)));
},
};
const mapToExcludeRegexp = (excludeByName: Record<string, boolean>): string | undefined => {
const mapToExcludeArray = (excludeByName: Record<string, boolean>): string[] => {
if (!excludeByName) {
return undefined;
return [];
}
const fieldsToExclude = Object.keys(excludeByName)
.filter(name => excludeByName[name])
.join('|');
if (fieldsToExclude.length === 0) {
return undefined;
}
return `^(${fieldsToExclude})$`;
return Object.keys(excludeByName).filter(name => excludeByName[name]);
};

View File

@ -16,16 +16,20 @@ export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOpti
byField: 'Time',
},
transformer: options => (data: DataFrame[]) => {
const regex = `/^(${options.byField})$/`;
const optionsArray = options.byField ? [options.byField] : [];
// not sure if I should use filterFieldsByNameTransformer to get the key field
const keyDataFrames = filterFieldsByNameTransformer.transformer({ include: regex })(data);
const keyDataFrames = filterFieldsByNameTransformer.transformer({
include: optionsArray,
})(data);
if (!keyDataFrames.length) {
// for now we only parse data frames with 2 fields
return data;
}
// not sure if I should use filterFieldsByNameTransformer to get the other fields
const otherDataFrames = filterFieldsByNameTransformer.transformer({ exclude: regex })(data);
const otherDataFrames = filterFieldsByNameTransformer.transformer({
exclude: optionsArray,
})(data);
if (!otherDataFrames.length) {
// for now we only parse data frames with 2 fields
return data;

View File

@ -0,0 +1,61 @@
import React, { useContext } from 'react';
import { stylesFactory, ThemeContext } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
import { IconButton } from '../IconButton/IconButton';
import { IconName } from '../../types';
interface FilterPillProps {
selected: boolean;
label: string;
onClick: React.MouseEventHandler<HTMLElement>;
icon?: IconName;
}
export const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick, icon = 'check' }) => {
const theme = useContext(ThemeContext);
const styles = getFilterPillStyles(theme, selected);
return (
<div className={styles.wrapper} onClick={onClick}>
<IconButton
name={icon}
onClick={e => {
e.stopPropagation();
onClick(e);
}}
className={styles.icon}
surface="header"
/>
<span className={styles.label}>{label}</span>
</div>
);
};
const getFilterPillStyles = stylesFactory((theme: GrafanaTheme, isSelected: boolean) => {
const labelColor = isSelected ? theme.colors.text : theme.colors.textWeak;
return {
wrapper: css`
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
background: ${theme.colors.bg2};
border-radius: ${theme.border.radius.sm};
display: inline-block;
padding: 0 ${theme.spacing.md} 0 ${theme.spacing.xs};
font-weight: ${theme.typography.weight.semibold};
font-size: ${theme.typography.size.sm};
color: ${theme.colors.text};
display: flex;
align-items: center;
height: 32px;
cursor: pointer;
`,
icon: css`
margin-right: ${theme.spacing.sm};
margin-left: ${theme.spacing.xs};
color: ${labelColor};
`,
label: css`
color: ${labelColor};
`,
};
});

View File

@ -337,7 +337,7 @@ export function SelectBase<T>({
width: width ? `${8 * width}px` : '100%',
}),
}}
className={cx('select-container', className)}
className={className}
{...commonSelectProps}
{...creatableProps}
{...asyncSelectProps}

View File

@ -40,8 +40,6 @@ interface ReactTableInternalState extends UseResizeColumnsState<{}>, UseSortBySt
function useTableStateReducer(props: Props) {
return useCallback(
(newState: ReactTableInternalState, action: any) => {
console.log(action, newState);
switch (action.type) {
case 'columnDoneResizing':
if (props.onColumnResize) {

View File

@ -1,23 +1,20 @@
import React, { useContext, ChangeEvent } from 'react';
import React, { ChangeEvent } from 'react';
import {
DataTransformerID,
CalculateFieldTransformerOptions,
DataTransformerID,
fieldReducers,
FieldType,
KeyValue,
ReducerID,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
FieldType,
ReducerID,
fieldReducers,
} from '@grafana/data';
import { ThemeContext } from '../../themes/ThemeContext';
import { css } from 'emotion';
import { InlineList } from '../List/InlineList';
import { Icon } from '../Icon/Icon';
import { Label } from '../Forms/Label';
import { StatsPicker } from '../StatsPicker/StatsPicker';
import { Switch } from '../Switch/Switch';
import { Switch } from '../Forms/Legacy/Switch/Switch';
import { Input } from '../Input/Input';
import { FilterPill } from '../FilterPill/FilterPill';
import { HorizontalGroup } from '../Layout/Layout';
interface CalculateFieldTransformerEditorProps extends TransformerUIProps<CalculateFieldTransformerOptions> {}
@ -98,7 +95,7 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
});
};
onToggleReplaceFields = (evt: ChangeEvent<HTMLInputElement>) => {
onToggleReplaceFields = () => {
const { options } = this.props;
this.props.onChange({
...options,
@ -125,75 +122,55 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
const { options } = this.props;
const { names, selected } = this.state;
const reducer = fieldReducers.get(options.reducer);
return (
<div>
<Label>Numeric Fields</Label>
<InlineList
items={names}
renderItem={(o, i) => {
return (
<span
className={css`
margin-right: ${i === names.length - 1 ? '0' : '10px'};
`}
>
<FilterPill
onClick={() => {
this.onFieldToggle(o);
}}
label={o}
selected={selected.indexOf(o) > -1}
/>
</span>
);
}}
/>
<Label>Calculation</Label>
<StatsPicker stats={[options.reducer]} onChange={this.onStatsChange} defaultStat={ReducerID.sum} />
<Label>Alias</Label>
<Input value={options.alias} placeholder={reducer.name} onChange={this.onAliasChanged} />
<Label>Replace all fields</Label>
<Switch checked={options.replaceFields} onChange={this.onToggleReplaceFields} />
{/* nullValueMode?: NullValueMode; */}
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Field name</div>
<HorizontalGroup spacing="xs">
{names.map((o, i) => {
return (
<FilterPill
key={`${o}/${i}`}
onClick={() => {
this.onFieldToggle(o);
}}
label={o}
selected={selected.indexOf(o) > -1}
/>
);
})}
</HorizontalGroup>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Calculation</div>
<StatsPicker stats={[options.reducer]} onChange={this.onStatsChange} defaultStat={ReducerID.sum} />
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Alias</div>
<Input value={options.alias} placeholder={reducer.name} onChange={this.onAliasChanged} />
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<Switch
label="Replace all fields"
labelClass="width-8"
checked={!!options.replaceFields}
onChange={this.onToggleReplaceFields}
/>
</div>
</div>
</div>
);
}
}
interface FilterPillProps {
selected: boolean;
label: string;
onClick: React.MouseEventHandler<HTMLElement>;
}
const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick }) => {
const theme = useContext(ThemeContext);
return (
<div
className={css`
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
color: white;
background: ${selected ? theme.palette.blue95 : theme.palette.blue77};
border-radius: 16px;
display: inline-block;
cursor: pointer;
`}
onClick={onClick}
>
{selected && (
<Icon
className={css`
margin-right: 4px;
`}
name="check"
/>
)}
{label}
</div>
);
};
export const calculateFieldTransformRegistryItem: TransformerRegistyItem<CalculateFieldTransformerOptions> = {
id: DataTransformerID.calculateField,
editor: CalculateFieldTransformerEditor,

View File

@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React from 'react';
import {
DataTransformerID,
FilterFieldsByNameTransformerOptions,
@ -7,17 +7,17 @@ import {
TransformerRegistyItem,
TransformerUIProps,
} from '@grafana/data';
import { ThemeContext } from '../../themes/ThemeContext';
import { css } from 'emotion';
import { InlineList } from '../List/InlineList';
import { Icon } from '../Icon/Icon';
import { HorizontalGroup } from '../Layout/Layout';
import { Input } from '../Input/Input';
import { FilterPill } from '../FilterPill/FilterPill';
interface FilterByNameTransformerEditorProps extends TransformerUIProps<FilterFieldsByNameTransformerOptions> {}
interface FilterByNameTransformerEditorState {
include: string;
include: string[];
options: FieldNameInfo[];
selected: string[];
regex?: string;
}
interface FieldNameInfo {
@ -31,7 +31,7 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
constructor(props: FilterByNameTransformerEditorProps) {
super(props);
this.state = {
include: props.options.include || '',
include: props.options.include || [],
options: [],
selected: [],
};
@ -43,10 +43,11 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
private initOptions() {
const { input, options } = this.props;
const configuredOptions = options.include ? options.include.split('|') : [];
const configuredOptions = options.include ? options.include : [];
const allNames: FieldNameInfo[] = [];
const byName: KeyValue<FieldNameInfo> = {};
for (const frame of input) {
for (const field of frame.fields) {
let v = byName[field.name];
@ -61,22 +62,28 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
}
}
let regexOption;
if (configuredOptions.length) {
const options: FieldNameInfo[] = [];
const selected: FieldNameInfo[] = [];
for (const v of allNames) {
if (configuredOptions.includes(v.name)) {
selected.push(v);
let selected: FieldNameInfo[] = [];
for (const o of configuredOptions) {
const selectedFields = allNames.filter(n => n.name === o);
if (selectedFields.length > 0) {
selected = selected.concat(selectedFields);
} else {
// there can be only one regex in the options
regexOption = o;
}
options.push(v);
}
this.setState({
options,
options: allNames,
selected: selected.map(s => s.name),
regex: regexOption,
});
} else {
this.setState({ options: allNames, selected: [] });
this.setState({ options: allNames, selected: allNames.map(n => n.name) });
}
}
@ -90,75 +97,57 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
};
onChange = (selected: string[]) => {
this.setState({ selected });
this.setState({ selected }, () => {
this.props.onChange({
...this.props.options,
include: this.state.regex ? [...selected, this.state.regex] : selected,
});
});
};
onInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const { selected, regex } = this.state;
this.props.onChange({
...this.props.options,
include: selected.join('|'),
include: regex ? [...selected, regex] : selected,
});
};
render() {
const { options, selected } = this.state;
return (
<>
<InlineList
items={options}
renderItem={(o, i) => {
const label = `${o.name}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
return (
<span
className={css`
margin-right: ${i === options.length - 1 ? '0' : '10px'};
`}
>
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Field name</div>
<HorizontalGroup spacing="xs">
<Input
placeholder="Regular expression pattern"
value={this.state.regex || ''}
onChange={e => this.setState({ regex: e.currentTarget.value })}
onBlur={this.onInputBlur}
width={25}
/>
{options.map((o, i) => {
const label = `${o.name}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
const isSelected = selected.indexOf(o.name) > -1;
return (
<FilterPill
key={`${o.name}/${i}`}
onClick={() => {
this.onFieldToggle(o.name);
}}
label={label}
selected={selected.indexOf(o.name) > -1}
selected={isSelected}
/>
</span>
);
}}
/>
</>
);
})}
</HorizontalGroup>
</div>
</div>
);
}
}
interface FilterPillProps {
selected: boolean;
label: string;
onClick: React.MouseEventHandler<HTMLElement>;
}
const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick }) => {
const theme = useContext(ThemeContext);
return (
<div
className={css`
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
color: white;
background: ${selected ? theme.palette.blue95 : theme.palette.blue77};
border-radius: 16px;
display: inline-block;
cursor: pointer;
`}
onClick={onClick}
>
{selected && (
<Icon
className={css`
margin-right: 4px;
`}
name="check"
/>
)}
{label}
</div>
);
};
export const filterFieldsByNameTransformRegistryItem: TransformerRegistyItem<FilterFieldsByNameTransformerOptions> = {
id: DataTransformerID.filterFieldsByName,
editor: FilterByNameTransformerEditor,

View File

@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React from 'react';
import {
DataTransformerID,
FilterFramesByRefIdTransformerOptions,
@ -7,10 +7,8 @@ import {
TransformerRegistyItem,
TransformerUIProps,
} from '@grafana/data';
import { ThemeContext } from '../../themes/ThemeContext';
import { css } from 'emotion';
import { InlineList } from '../List/InlineList';
import { Icon } from '../Icon/Icon';
import { HorizontalGroup } from '../Layout/Layout';
import { FilterPill } from '../FilterPill/FilterPill';
interface FilterByRefIdTransformerEditorProps extends TransformerUIProps<FilterFramesByRefIdTransformerOptions> {}
@ -100,65 +98,31 @@ export class FilterByRefIdTransformerEditor extends React.PureComponent<
render() {
const { options, selected } = this.state;
return (
<>
<InlineList
items={options}
renderItem={(o, i) => {
const label = `${o.refId}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
return (
<span
className={css`
margin-right: ${i === options.length - 1 ? '0' : '10px'};
`}
>
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Series refId</div>
<HorizontalGroup spacing="xs">
{options.map((o, i) => {
const label = `${o.refId}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
const isSelected = selected.indexOf(o.refId) > -1;
return (
<FilterPill
key={`${o.refId}/${i}`}
onClick={() => {
this.onFieldToggle(o.refId);
}}
label={label}
selected={selected.indexOf(o.refId) > -1}
selected={isSelected}
/>
</span>
);
}}
/>
</>
);
})}
</HorizontalGroup>
</div>
</div>
);
}
}
interface FilterPillProps {
selected: boolean;
label: string;
onClick: React.MouseEventHandler<HTMLElement>;
}
const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick }) => {
const theme = useContext(ThemeContext);
return (
<div
className={css`
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
color: white;
background: ${selected ? theme.palette.blue95 : theme.palette.blue77};
border-radius: 16px;
display: inline-block;
cursor: pointer;
`}
onClick={onClick}
>
{selected && (
<Icon
className={css`
margin-right: 4px;
`}
name="check"
/>
)}
{label}
</div>
);
};
export const filterFramesByRefIdTransformRegistryItem: TransformerRegistyItem<FilterFramesByRefIdTransformerOptions> = {
id: DataTransformerID.filterByRefId,
editor: FilterByRefIdTransformerEditor,

View File

@ -1,11 +1,11 @@
import React, { useMemo, useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import {
DataTransformerID,
SelectableValue,
SeriesToColumnsOptions,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
SeriesToColumnsOptions,
SelectableValue,
} from '@grafana/data';
import { getAllFieldNamesFromDataFrames } from './OrganizeFieldsTransformerEditor';
import { Select } from '../Select/Select';
@ -30,9 +30,9 @@ export const SeriesToFieldsTransformerEditor: React.FC<TransformerUIProps<Series
return (
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label">Field</div>
<Select options={fieldNameOptions} value={options.byField} onChange={onSelectField} />
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Field name</div>
<Select options={fieldNameOptions} value={options.byField} onChange={onSelectField} isClearable />
</div>
</div>
);