Commit 11fa7c69 by Ryan McKinley Committed by GitHub

Transformers: add search to transform selection (#30854)

parent 48334ab8
...@@ -122,6 +122,7 @@ export const Components = { ...@@ -122,6 +122,7 @@ export const Components = {
modeLabel: 'Transform mode label', modeLabel: 'Transform mode label',
calculationsLabel: 'Transform calculations label', calculationsLabel: 'Transform calculations label',
}, },
searchInput: 'search transformations',
}, },
PageToolbar: { PageToolbar: {
container: () => '.page-toolbar', container: () => '.page-toolbar',
......
...@@ -51,8 +51,8 @@ describe('TransformationsEditor', () => { ...@@ -51,8 +51,8 @@ describe('TransformationsEditor', () => {
const addTransformationButton = screen.getByText(buttonLabel); const addTransformationButton = screen.getByText(buttonLabel);
userEvent.click(addTransformationButton); userEvent.click(addTransformationButton);
const picker = screen.getByLabelText(selectors.components.ValuePicker.select(buttonLabel)); const search = screen.getByLabelText(selectors.components.Transforms.searchInput);
expect(picker).toBeDefined(); expect(search).toBeDefined();
}); });
}); });
......
import React from 'react'; import React, { ChangeEvent } from 'react';
import { import {
Alert, Alert,
Button, Button,
...@@ -8,9 +8,10 @@ import { ...@@ -8,9 +8,10 @@ import {
Themeable, Themeable,
DismissableFeatureInfoBox, DismissableFeatureInfoBox,
useTheme, useTheme,
ValuePicker,
VerticalGroup, VerticalGroup,
withTheme, withTheme,
Input,
IconButton,
} from '@grafana/ui'; } from '@grafana/ui';
import { import {
DataFrame, DataFrame,
...@@ -40,6 +41,8 @@ interface TransformationsEditorProps extends Themeable { ...@@ -40,6 +41,8 @@ interface TransformationsEditorProps extends Themeable {
interface State { interface State {
data: DataFrame[]; data: DataFrame[];
transformations: TransformationsEditorTransformation[]; transformations: TransformationsEditorTransformation[];
search: string;
showPicker?: boolean;
} }
class UnThemedTransformationsEditor extends React.PureComponent<TransformationsEditorProps, State> { class UnThemedTransformationsEditor extends React.PureComponent<TransformationsEditorProps, State> {
...@@ -56,9 +59,34 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE ...@@ -56,9 +59,34 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
id: ids[i], id: ids[i],
})), })),
data: [], 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[]) { buildTransformationIds(transformations: DataTransformerConfig[]) {
const transformationCounters: Record<string, number> = {}; const transformationCounters: Record<string, number> = {};
const transformationIds: string[] = []; const transformationIds: string[] = [];
...@@ -113,6 +141,7 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE ...@@ -113,6 +141,7 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
const { transformations } = this.state; const { transformations } = this.state;
const nextId = this.getTransformationNextId(selectable.value!); const nextId = this.getTransformationNextId(selectable.value!);
this.setState({ search: '', showPicker: false });
this.onChange([ this.onChange([
...transformations, ...transformations,
{ {
...@@ -139,33 +168,6 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE ...@@ -139,33 +168,6 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
this.onChange(next); 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) => { onDragEnd = (result: DropResult) => {
const { transformations } = this.state; const { transformations } = this.state;
...@@ -208,43 +210,106 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE ...@@ -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 ( return (
<> <>
<Container grow={1}> {noTransforms && (
<DismissableFeatureInfoBox <Container grow={1}>
title="Transformations" <DismissableFeatureInfoBox
className={css` title="Transformations"
margin-bottom: ${this.props.theme.spacing.lg}; className={css`
`} margin-bottom: ${this.props.theme.spacing.lg};
persistenceId="transformationsFeaturesInfoBox" `}
url={getDocsLink(DocsId.Transformations)} 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> Add transformation
Transformations allow you to join, calculate, re-order, hide and rename your query results before being </Button>
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>
</> </>
); );
} }
...@@ -271,9 +336,8 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE ...@@ -271,9 +336,8 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
title="Transformations can't be used on a panel with alerts" title="Transformations can't be used on a panel with alerts"
/> />
) : null} ) : null}
{!hasTransforms && this.renderNoAddedTransformsState()}
{hasTransforms && this.renderTransformationEditors()} {hasTransforms && this.renderTransformationEditors()}
{hasTransforms && this.renderTransformationSelector()} {this.renderTransformsPicker()}
</div> </div>
</Container> </Container>
</CustomScrollbar> </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