From fe28e2a6b11ae730f7d5ed580894663ee5884743 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 23 Apr 2020 10:12:06 +0200 Subject: [PATCH] Transformations: UI tweaks, filter by name regex validation (#23800) * Add validation to filter by name regex, minor layout tweaks * Use cards uin for non configured transformations --- .../transformations/matchers/nameMatcher.ts | 7 +- .../src/components/Layout/Layout.tsx | 41 +++++++++--- .../CalculateFieldTransformerEditor.tsx | 2 +- .../FilterByNameTransformerEditor.tsx | 63 ++++++++++++++---- .../FilterByRefIdTransformerEditor.tsx | 2 +- public/app/core/components/Card/Card.tsx | 27 ++++++++ .../TransformationsEditor.tsx | 65 ++++++++++++++++--- .../datasources/NewDataSourcePage.tsx | 54 +++++++-------- 8 files changed, 198 insertions(+), 63 deletions(-) create mode 100644 public/app/core/components/Card/Card.tsx diff --git a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts index 1061c2d8047..1b9c0e377ae 100644 --- a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts +++ b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts @@ -11,7 +11,12 @@ const fieldNameMacher: FieldMatcherInfo = { defaultOptions: '/.*/', get: (pattern: string) => { - const regex = stringToJsRegex(pattern); + let regex = new RegExp(''); + try { + regex = stringToJsRegex(pattern); + } catch (e) { + console.error(e); + } return (field: Field) => { return regex.test(field.name); }; diff --git a/packages/grafana-ui/src/components/Layout/Layout.tsx b/packages/grafana-ui/src/components/Layout/Layout.tsx index b4dfac7105d..8114a4a0762 100644 --- a/packages/grafana-ui/src/components/Layout/Layout.tsx +++ b/packages/grafana-ui/src/components/Layout/Layout.tsx @@ -7,7 +7,7 @@ enum Orientation { Horizontal, Vertical, } -type Spacing = 'xs' | 'sm' | 'md' | 'lg'; +type Spacing = 'none' | 'xs' | 'sm' | 'md' | 'lg'; type Justify = 'flex-start' | 'flex-end' | 'space-between' | 'center'; type Align = 'normal' | 'flex-start' | 'flex-end' | 'center'; @@ -18,6 +18,7 @@ export interface LayoutProps { justify?: Justify; align?: Align; width?: string; + wrap?: boolean; } export interface ContainerProps { @@ -31,10 +32,11 @@ export const Layout: React.FC = ({ spacing = 'sm', justify = 'flex-start', align = 'normal', + wrap = false, width = 'auto', }) => { const theme = useTheme(); - const styles = getStyles(theme, orientation, spacing, justify, align); + const styles = getStyles(theme, orientation, spacing, justify, align, wrap); return (
{React.Children.toArray(children) @@ -55,13 +57,26 @@ export const HorizontalGroup: React.FC> = ({ spacing, justify, align = 'center', + wrap, width, }) => ( - + {children} ); -export const VerticalGroup: React.FC> = ({ children, spacing, justify, width }) => ( +export const VerticalGroup: React.FC> = ({ + children, + spacing, + justify, + width, +}) => ( {children} @@ -74,22 +89,28 @@ export const Container: React.FC = ({ children, padding, margin }; const getStyles = stylesFactory( - (theme: GrafanaTheme, orientation: Orientation, spacing: Spacing, justify: Justify, align) => { + (theme: GrafanaTheme, orientation: Orientation, spacing: Spacing, justify: Justify, align, wrap) => { + const finalSpacing = spacing !== 'none' ? theme.spacing[spacing] : 0; + const marginCompensation = orientation === Orientation.Horizontal && !wrap ? 0 : `-${finalSpacing}`; + return { layout: css` display: flex; flex-direction: ${orientation === Orientation.Vertical ? 'column' : 'row'}; + flex-wrap: ${wrap ? 'wrap' : 'nowrap'}; justify-content: ${justify}; align-items: ${align}; height: 100%; max-width: 100%; + // compensate for last row margin when wrapped, horizontal layout + margin-bottom: ${marginCompensation}; `, childWrapper: css` - margin-bottom: ${orientation === Orientation.Horizontal ? 0 : theme.spacing[spacing]}; - margin-right: ${orientation === Orientation.Horizontal ? theme.spacing[spacing] : 0}; + margin-bottom: ${orientation === Orientation.Horizontal && !wrap ? 0 : finalSpacing}; + margin-right: ${orientation === Orientation.Horizontal ? finalSpacing : 0}; display: flex; align-items: ${align}; - height: 100%; + // height: 100%; &:last-child { margin-bottom: 0; @@ -101,8 +122,8 @@ const getStyles = stylesFactory( ); const getContainerStyles = stylesFactory((theme: GrafanaTheme, padding?: Spacing, margin?: Spacing) => { - const paddingSize = (padding && theme.spacing[padding]) || 0; - const marginSize = (margin && theme.spacing[margin]) || 0; + const paddingSize = (padding && padding !== 'none' && theme.spacing[padding]) || 0; + const marginSize = (margin && margin !== 'none' && theme.spacing[margin]) || 0; return { wrapper: css` margin: ${marginSize}; diff --git a/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx b/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx index 7d9942cf4e4..6cb54bea70b 100644 --- a/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx +++ b/packages/grafana-ui/src/components/TransformersUI/CalculateFieldTransformerEditor.tsx @@ -128,7 +128,7 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
Field name
- + {names.map((o, i) => { return ( {} @@ -18,6 +20,7 @@ interface FilterByNameTransformerEditorState { options: FieldNameInfo[]; selected: string[]; regex?: string; + isRegexValid?: boolean; } interface FieldNameInfo { @@ -34,6 +37,7 @@ export class FilterByNameTransformerEditor extends React.PureComponent< include: props.options.include || [], options: [], selected: [], + isRegexValid: true, }; } @@ -97,36 +101,69 @@ export class FilterByNameTransformerEditor extends React.PureComponent< }; onChange = (selected: string[]) => { + const { regex, isRegexValid } = this.state; + let include = selected; + + if (regex && isRegexValid) { + include = include.concat([regex]); + } + this.setState({ selected }, () => { this.props.onChange({ ...this.props.options, - include: this.state.regex ? [...selected, this.state.regex] : selected, + include, }); }); }; onInputBlur = (e: React.FocusEvent) => { const { selected, regex } = this.state; - this.props.onChange({ - ...this.props.options, - include: regex ? [...selected, regex] : selected, + let isRegexValid = true; + try { + if (regex) { + new RegExp(regex); + } + } catch (e) { + isRegexValid = false; + } + if (isRegexValid) { + this.props.onChange({ + ...this.props.options, + include: regex ? [...selected, regex] : selected, + }); + } else { + this.props.onChange({ + ...this.props.options, + include: selected, + }); + } + this.setState({ + isRegexValid, }); }; render() { - const { options, selected } = this.state; + const { options, selected, isRegexValid } = this.state; return (
Field name
- - this.setState({ regex: e.currentTarget.value })} - onBlur={this.onInputBlur} - width={25} - /> + + + 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; diff --git a/packages/grafana-ui/src/components/TransformersUI/FilterByRefIdTransformerEditor.tsx b/packages/grafana-ui/src/components/TransformersUI/FilterByRefIdTransformerEditor.tsx index 51108458a00..b8235ab1f96 100644 --- a/packages/grafana-ui/src/components/TransformersUI/FilterByRefIdTransformerEditor.tsx +++ b/packages/grafana-ui/src/components/TransformersUI/FilterByRefIdTransformerEditor.tsx @@ -101,7 +101,7 @@ export class FilterByRefIdTransformerEditor extends React.PureComponent<
Series refId
- + {options.map((o, i) => { const label = `${o.refId}${o.count > 1 ? ' (' + o.count + ')' : ''}`; const isSelected = selected.indexOf(o.refId) > -1; diff --git a/public/app/core/components/Card/Card.tsx b/public/app/core/components/Card/Card.tsx new file mode 100644 index 00000000000..0a1ee07f175 --- /dev/null +++ b/public/app/core/components/Card/Card.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { cx } from 'emotion'; + +export interface CardProps { + logoUrl?: string; + title: string; + description?: string; + actions?: React.ReactNode; + onClick?: () => void; + ariaLabel?: string; + className?: string; +} + +export const Card: React.FC = ({ logoUrl, title, description, actions, onClick, ariaLabel, className }) => { + const mainClassName = cx('add-data-source-item', className); + + return ( +
+ {logoUrl && } +
+ {title} + {description && {description}} +
+ {actions &&
{actions}
} +
+ ); +}; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index 3879792d4bb..c1fa63a7268 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -1,13 +1,25 @@ import React from 'react'; -import { Container, CustomScrollbar, ValuePicker } from '@grafana/ui'; +import { + Container, + CustomScrollbar, + InfoBox, + ValuePicker, + Button, + useTheme, + VerticalGroup, + stylesFactory, +} from '@grafana/ui'; import { DataFrame, DataTransformerConfig, + GrafanaTheme, SelectableValue, standardTransformersRegistry, transformDataFrame, } from '@grafana/data'; import { TransformationOperationRow } from './TransformationOperationRow'; +import { Card, CardProps } from '../../../../core/components/Card/Card'; +import { css } from 'emotion'; interface Props { onChange: (transformations: DataTransformerConfig[]) => void; @@ -52,10 +64,10 @@ export class TransformationsEditor extends React.PureComponent { return ( @@ -109,17 +121,54 @@ export class TransformationsEditor extends React.PureComponent { }; render() { + const hasTransformationsConfigured = this.props.transformations.length > 0; return ( -

- Transformations allow you to combine, re-order, hide and rename specific parts the the data set before being - visualized. -

- {this.renderTransformationEditors()} - {this.renderTransformationSelector()} + {!hasTransformationsConfigured && ( + +

+ Transformations allow you to combine, re-order, hide and rename specific parts the the data set before + being visualized. Choose one of the transformations below to start with: +

+ + {standardTransformersRegistry.list().map(t => { + return ( + Select} + onClick={() => { + this.onTransformationAdd({ value: t.id }); + }} + /> + ); + })} + +
+ )} + {hasTransformationsConfigured && this.renderTransformationEditors()} + {hasTransformationsConfigured && this.renderTransformationSelector()}
); } } + +const TransformationCard: React.FC = props => { + const theme = useTheme(); + const styles = getTransformationCardStyles(theme); + return ; +}; + +const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => { + return { + card: css` + background: ${theme.colors.bg2}; + width: 100%; + &:hover { + background: ${theme.colors.bg3}; + } + `, + }; +}); diff --git a/public/app/features/datasources/NewDataSourcePage.tsx b/public/app/features/datasources/NewDataSourcePage.tsx index 05bef67d1d2..8fb0d426020 100644 --- a/public/app/features/datasources/NewDataSourcePage.tsx +++ b/public/app/features/datasources/NewDataSourcePage.tsx @@ -1,5 +1,4 @@ import React, { FC, PureComponent } from 'react'; -import classNames from 'classnames'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import { DataSourcePluginMeta, NavModel } from '@grafana/data'; @@ -12,6 +11,7 @@ import { addDataSource, loadDataSourcePlugins } from './state/actions'; import { getDataSourcePlugins } from './state/selectors'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { setDataSourceTypeSearchQuery } from './state/reducers'; +import { Card } from 'app/core/components/Card/Card'; export interface Props { navModel: NavModel; @@ -120,37 +120,33 @@ const DataSourceTypeCard: FC = props => { // find first plugin info link const learnMoreLink = plugin.info.links && plugin.info.links.length > 0 ? plugin.info.links[0] : null; - const mainClassName = classNames('add-data-source-item', { - 'add-data-source-item--phantom': isPhantom, - }); return ( -
+ {learnMoreLink && ( + + {learnMoreLink.name} + + )} + {!isPhantom && } + + } + className={isPhantom && 'add-data-source-item--phantom'} onClick={onClick} - aria-label={e2e.pages.AddDataSource.selectors.dataSourcePlugins(plugin.name)} - > - -
- {plugin.name} - {plugin.info.description && {plugin.info.description}} -
-
- {learnMoreLink && ( - - {learnMoreLink.name} - - )} - {!isPhantom && } -
-
+ /> ); };