Commit 11fa7c69 by Ryan McKinley Committed by GitHub

Transformers: add search to transform selection (#30854)

parent 48334ab8
......@@ -122,6 +122,7 @@ export const Components = {
modeLabel: 'Transform mode label',
calculationsLabel: 'Transform calculations label',
},
searchInput: 'search transformations',
},
PageToolbar: {
container: () => '.page-toolbar',
......
......@@ -51,8 +51,8 @@ describe('TransformationsEditor', () => {
const addTransformationButton = screen.getByText(buttonLabel);
userEvent.click(addTransformationButton);
const picker = screen.getByLabelText(selectors.components.ValuePicker.select(buttonLabel));
expect(picker).toBeDefined();
const search = screen.getByLabelText(selectors.components.Transforms.searchInput);
expect(search).toBeDefined();
});
});
......
import React from 'react';
import React, { ChangeEvent } from 'react';
import {
Alert,
Button,
......@@ -8,9 +8,10 @@ import {
Themeable,
DismissableFeatureInfoBox,
useTheme,
ValuePicker,
VerticalGroup,
withTheme,
Input,
IconButton,
} from '@grafana/ui';
import {
DataFrame,
......@@ -40,6 +41,8 @@ interface TransformationsEditorProps extends Themeable {
interface State {
data: DataFrame[];
transformations: TransformationsEditorTransformation[];
search: string;
showPicker?: boolean;
}
class UnThemedTransformationsEditor extends React.PureComponent<TransformationsEditorProps, State> {
......@@ -56,9 +59,34 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
id: ids[i],
})),
data: [],
search: '',
};
}
onSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ search: event.target.value });
};
onSearchKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
const { search } = this.state;
if (search) {
const lower = search.toLowerCase();
const filtered = standardTransformersRegistry.list().filter((t) => {
const txt = (t.name + t.description).toLowerCase();
return txt.indexOf(lower) >= 0;
});
if (filtered.length > 0) {
this.onTransformationAdd({ value: filtered[0].id });
}
}
} else if (event.keyCode === 27) {
// Escape key
this.setState({ search: '', showPicker: false });
event.stopPropagation(); // don't exit the editor
}
};
buildTransformationIds(transformations: DataTransformerConfig[]) {
const transformationCounters: Record<string, number> = {};
const transformationIds: string[] = [];
......@@ -113,6 +141,7 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
const { transformations } = this.state;
const nextId = this.getTransformationNextId(selectable.value!);
this.setState({ search: '', showPicker: false });
this.onChange([
...transformations,
{
......@@ -139,33 +168,6 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
this.onChange(next);
};
renderTransformationSelector = () => {
const availableTransformers = standardTransformersRegistry.list().map((t) => {
return {
value: t.transformation.id,
label: t.name,
description: t.description,
};
});
return (
<div
className={css`
max-width: 66%;
`}
>
<ValuePicker
size="md"
variant="secondary"
label="Add transformation"
options={availableTransformers}
onChange={this.onTransformationAdd}
isFullWidth={false}
/>
</div>
);
};
onDragEnd = (result: DropResult) => {
const { transformations } = this.state;
......@@ -208,43 +210,106 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
);
};
renderNoAddedTransformsState() {
renderTransformsPicker() {
const { transformations, search } = this.state;
let suffix: React.ReactNode = null;
let xforms = standardTransformersRegistry.list();
if (search) {
const lower = search.toLowerCase();
const filtered = xforms.filter((t) => {
const txt = (t.name + t.description).toLowerCase();
return txt.indexOf(lower) >= 0;
});
suffix = (
<>
{filtered.length} / {xforms.length} &nbsp;&nbsp;
<IconButton
name="times"
surface="header"
onClick={() => {
this.setState({ search: '' });
}}
/>
</>
);
xforms = filtered;
}
const noTransforms = !transformations?.length;
const showPicker = noTransforms || this.state.showPicker;
if (!suffix && showPicker && !noTransforms) {
suffix = (
<IconButton
name="times"
surface="header"
onClick={() => {
this.setState({ showPicker: false });
}}
/>
);
}
return (
<>
<Container grow={1}>
<DismissableFeatureInfoBox
title="Transformations"
className={css`
margin-bottom: ${this.props.theme.spacing.lg};
`}
persistenceId="transformationsFeaturesInfoBox"
url={getDocsLink(DocsId.Transformations)}
{noTransforms && (
<Container grow={1}>
<DismissableFeatureInfoBox
title="Transformations"
className={css`
margin-bottom: ${this.props.theme.spacing.lg};
`}
persistenceId="transformationsFeaturesInfoBox"
url={getDocsLink(DocsId.Transformations)}
>
<p>
Transformations allow you to join, calculate, re-order, hide and rename your query results before being
visualized. <br />
Many transforms are not suitable if you&apos;re using the Graph visualization as it currently only
supports time series. <br />
It can help to switch to Table visualization to understand what a transformation is doing. <br />
</p>
</DismissableFeatureInfoBox>
</Container>
)}
{showPicker ? (
<VerticalGroup>
<Input
aria-label={selectors.components.Transforms.searchInput}
value={search ?? ''}
autoFocus={!noTransforms}
placeholder="Add transformation"
onChange={this.onSearchChange}
onKeyDown={this.onSearchKeyDown}
suffix={suffix}
/>
{xforms.map((t) => {
return (
<TransformationCard
key={t.name}
title={t.name}
description={t.description}
actions={<Button>Select</Button>}
ariaLabel={selectors.components.TransformTab.newTransform(t.name)}
onClick={() => {
this.onTransformationAdd({ value: t.id });
}}
/>
);
})}
</VerticalGroup>
) : (
<Button
icon="plus"
variant="secondary"
onClick={() => {
this.setState({ showPicker: true });
}}
>
<p>
Transformations allow you to join, calculate, re-order, hide and rename your query results before being
visualized. <br />
Many transforms are not suitable if you&apos;re using the Graph visualization as it currently only
supports time series. <br />
It can help to switch to Table visualization to understand what a transformation is doing. <br />
</p>
</DismissableFeatureInfoBox>
</Container>
<VerticalGroup>
{standardTransformersRegistry.list().map((t) => {
return (
<TransformationCard
key={t.name}
title={t.name}
description={t.description}
actions={<Button>Select</Button>}
ariaLabel={selectors.components.TransformTab.newTransform(t.name)}
onClick={() => {
this.onTransformationAdd({ value: t.id });
}}
/>
);
})}
</VerticalGroup>
Add transformation
</Button>
)}
</>
);
}
......@@ -271,9 +336,8 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
title="Transformations can't be used on a panel with alerts"
/>
) : null}
{!hasTransforms && this.renderNoAddedTransformsState()}
{hasTransforms && this.renderTransformationEditors()}
{hasTransforms && this.renderTransformationSelector()}
{this.renderTransformsPicker()}
</div>
</Container>
</CustomScrollbar>
......
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