mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
@@ -11,7 +11,12 @@ const fieldNameMacher: FieldMatcherInfo<string> = {
|
|||||||
defaultOptions: '/.*/',
|
defaultOptions: '/.*/',
|
||||||
|
|
||||||
get: (pattern: string) => {
|
get: (pattern: string) => {
|
||||||
const regex = stringToJsRegex(pattern);
|
let regex = new RegExp('');
|
||||||
|
try {
|
||||||
|
regex = stringToJsRegex(pattern);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
return (field: Field) => {
|
return (field: Field) => {
|
||||||
return regex.test(field.name);
|
return regex.test(field.name);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ enum Orientation {
|
|||||||
Horizontal,
|
Horizontal,
|
||||||
Vertical,
|
Vertical,
|
||||||
}
|
}
|
||||||
type Spacing = 'xs' | 'sm' | 'md' | 'lg';
|
type Spacing = 'none' | 'xs' | 'sm' | 'md' | 'lg';
|
||||||
type Justify = 'flex-start' | 'flex-end' | 'space-between' | 'center';
|
type Justify = 'flex-start' | 'flex-end' | 'space-between' | 'center';
|
||||||
type Align = 'normal' | 'flex-start' | 'flex-end' | 'center';
|
type Align = 'normal' | 'flex-start' | 'flex-end' | 'center';
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ export interface LayoutProps {
|
|||||||
justify?: Justify;
|
justify?: Justify;
|
||||||
align?: Align;
|
align?: Align;
|
||||||
width?: string;
|
width?: string;
|
||||||
|
wrap?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerProps {
|
export interface ContainerProps {
|
||||||
@@ -31,10 +32,11 @@ export const Layout: React.FC<LayoutProps> = ({
|
|||||||
spacing = 'sm',
|
spacing = 'sm',
|
||||||
justify = 'flex-start',
|
justify = 'flex-start',
|
||||||
align = 'normal',
|
align = 'normal',
|
||||||
|
wrap = false,
|
||||||
width = 'auto',
|
width = 'auto',
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getStyles(theme, orientation, spacing, justify, align);
|
const styles = getStyles(theme, orientation, spacing, justify, align, wrap);
|
||||||
return (
|
return (
|
||||||
<div className={styles.layout} style={{ width }}>
|
<div className={styles.layout} style={{ width }}>
|
||||||
{React.Children.toArray(children)
|
{React.Children.toArray(children)
|
||||||
@@ -55,13 +57,26 @@ export const HorizontalGroup: React.FC<Omit<LayoutProps, 'orientation'>> = ({
|
|||||||
spacing,
|
spacing,
|
||||||
justify,
|
justify,
|
||||||
align = 'center',
|
align = 'center',
|
||||||
|
wrap,
|
||||||
width,
|
width,
|
||||||
}) => (
|
}) => (
|
||||||
<Layout spacing={spacing} justify={justify} orientation={Orientation.Horizontal} align={align} width={width}>
|
<Layout
|
||||||
|
spacing={spacing}
|
||||||
|
justify={justify}
|
||||||
|
orientation={Orientation.Horizontal}
|
||||||
|
align={align}
|
||||||
|
width={width}
|
||||||
|
wrap={wrap}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
export const VerticalGroup: React.FC<Omit<LayoutProps, 'orientation'>> = ({ children, spacing, justify, width }) => (
|
export const VerticalGroup: React.FC<Omit<LayoutProps, 'orientation' | 'wrap'>> = ({
|
||||||
|
children,
|
||||||
|
spacing,
|
||||||
|
justify,
|
||||||
|
width,
|
||||||
|
}) => (
|
||||||
<Layout spacing={spacing} justify={justify} orientation={Orientation.Vertical} width={width}>
|
<Layout spacing={spacing} justify={justify} orientation={Orientation.Vertical} width={width}>
|
||||||
{children}
|
{children}
|
||||||
</Layout>
|
</Layout>
|
||||||
@@ -74,22 +89,28 @@ export const Container: React.FC<ContainerProps> = ({ children, padding, margin
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = stylesFactory(
|
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 {
|
return {
|
||||||
layout: css`
|
layout: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: ${orientation === Orientation.Vertical ? 'column' : 'row'};
|
flex-direction: ${orientation === Orientation.Vertical ? 'column' : 'row'};
|
||||||
|
flex-wrap: ${wrap ? 'wrap' : 'nowrap'};
|
||||||
justify-content: ${justify};
|
justify-content: ${justify};
|
||||||
align-items: ${align};
|
align-items: ${align};
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
// compensate for last row margin when wrapped, horizontal layout
|
||||||
|
margin-bottom: ${marginCompensation};
|
||||||
`,
|
`,
|
||||||
childWrapper: css`
|
childWrapper: css`
|
||||||
margin-bottom: ${orientation === Orientation.Horizontal ? 0 : theme.spacing[spacing]};
|
margin-bottom: ${orientation === Orientation.Horizontal && !wrap ? 0 : finalSpacing};
|
||||||
margin-right: ${orientation === Orientation.Horizontal ? theme.spacing[spacing] : 0};
|
margin-right: ${orientation === Orientation.Horizontal ? finalSpacing : 0};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: ${align};
|
align-items: ${align};
|
||||||
height: 100%;
|
// height: 100%;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@@ -101,8 +122,8 @@ const getStyles = stylesFactory(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getContainerStyles = stylesFactory((theme: GrafanaTheme, padding?: Spacing, margin?: Spacing) => {
|
const getContainerStyles = stylesFactory((theme: GrafanaTheme, padding?: Spacing, margin?: Spacing) => {
|
||||||
const paddingSize = (padding && theme.spacing[padding]) || 0;
|
const paddingSize = (padding && padding !== 'none' && theme.spacing[padding]) || 0;
|
||||||
const marginSize = (margin && theme.spacing[margin]) || 0;
|
const marginSize = (margin && margin !== 'none' && theme.spacing[margin]) || 0;
|
||||||
return {
|
return {
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
margin: ${marginSize};
|
margin: ${marginSize};
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
|
|||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
<div className="gf-form-label width-8">Field name</div>
|
<div className="gf-form-label width-8">Field name</div>
|
||||||
<HorizontalGroup spacing="xs">
|
<HorizontalGroup spacing="xs" align="flex-start" wrap>
|
||||||
{names.map((o, i) => {
|
{names.map((o, i) => {
|
||||||
return (
|
return (
|
||||||
<FilterPill
|
<FilterPill
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
import { HorizontalGroup } from '../Layout/Layout';
|
import { HorizontalGroup } from '../Layout/Layout';
|
||||||
import { Input } from '../Input/Input';
|
import { Input } from '../Input/Input';
|
||||||
import { FilterPill } from '../FilterPill/FilterPill';
|
import { FilterPill } from '../FilterPill/FilterPill';
|
||||||
|
import { Field } from '../Forms/Field';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
|
||||||
interface FilterByNameTransformerEditorProps extends TransformerUIProps<FilterFieldsByNameTransformerOptions> {}
|
interface FilterByNameTransformerEditorProps extends TransformerUIProps<FilterFieldsByNameTransformerOptions> {}
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ interface FilterByNameTransformerEditorState {
|
|||||||
options: FieldNameInfo[];
|
options: FieldNameInfo[];
|
||||||
selected: string[];
|
selected: string[];
|
||||||
regex?: string;
|
regex?: string;
|
||||||
|
isRegexValid?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FieldNameInfo {
|
interface FieldNameInfo {
|
||||||
@@ -34,6 +37,7 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
|
|||||||
include: props.options.include || [],
|
include: props.options.include || [],
|
||||||
options: [],
|
options: [],
|
||||||
selected: [],
|
selected: [],
|
||||||
|
isRegexValid: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,36 +101,69 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
|
|||||||
};
|
};
|
||||||
|
|
||||||
onChange = (selected: string[]) => {
|
onChange = (selected: string[]) => {
|
||||||
|
const { regex, isRegexValid } = this.state;
|
||||||
|
let include = selected;
|
||||||
|
|
||||||
|
if (regex && isRegexValid) {
|
||||||
|
include = include.concat([regex]);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ selected }, () => {
|
this.setState({ selected }, () => {
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
...this.props.options,
|
...this.props.options,
|
||||||
include: this.state.regex ? [...selected, this.state.regex] : selected,
|
include,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
onInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
const { selected, regex } = this.state;
|
const { selected, regex } = this.state;
|
||||||
this.props.onChange({
|
let isRegexValid = true;
|
||||||
...this.props.options,
|
try {
|
||||||
include: regex ? [...selected, regex] : selected,
|
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() {
|
render() {
|
||||||
const { options, selected } = this.state;
|
const { options, selected, isRegexValid } = this.state;
|
||||||
return (
|
return (
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
<div className="gf-form-label width-8">Field name</div>
|
<div className="gf-form-label width-8">Field name</div>
|
||||||
<HorizontalGroup spacing="xs">
|
<HorizontalGroup spacing="xs" align="flex-start" wrap>
|
||||||
<Input
|
<Field
|
||||||
placeholder="Regular expression pattern"
|
invalid={!isRegexValid}
|
||||||
value={this.state.regex || ''}
|
error={!isRegexValid ? 'Invalid pattern' : undefined}
|
||||||
onChange={e => this.setState({ regex: e.currentTarget.value })}
|
className={css`
|
||||||
onBlur={this.onInputBlur}
|
margin-bottom: 0;
|
||||||
width={25}
|
`}
|
||||||
/>
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Regular expression pattern"
|
||||||
|
value={this.state.regex || ''}
|
||||||
|
onChange={e => this.setState({ regex: e.currentTarget.value })}
|
||||||
|
onBlur={this.onInputBlur}
|
||||||
|
width={25}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
{options.map((o, i) => {
|
{options.map((o, i) => {
|
||||||
const label = `${o.name}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
|
const label = `${o.name}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
|
||||||
const isSelected = selected.indexOf(o.name) > -1;
|
const isSelected = selected.indexOf(o.name) > -1;
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export class FilterByRefIdTransformerEditor extends React.PureComponent<
|
|||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
<div className="gf-form-label width-8">Series refId</div>
|
<div className="gf-form-label width-8">Series refId</div>
|
||||||
<HorizontalGroup spacing="xs">
|
<HorizontalGroup spacing="xs" align="flex-start" wrap>
|
||||||
{options.map((o, i) => {
|
{options.map((o, i) => {
|
||||||
const label = `${o.refId}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
|
const label = `${o.refId}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
|
||||||
const isSelected = selected.indexOf(o.refId) > -1;
|
const isSelected = selected.indexOf(o.refId) > -1;
|
||||||
|
|||||||
27
public/app/core/components/Card/Card.tsx
Normal file
27
public/app/core/components/Card/Card.tsx
Normal file
@@ -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<CardProps> = ({ logoUrl, title, description, actions, onClick, ariaLabel, className }) => {
|
||||||
|
const mainClassName = cx('add-data-source-item', className);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mainClassName} onClick={onClick} aria-label={ariaLabel}>
|
||||||
|
{logoUrl && <img className="add-data-source-item-logo" src={logoUrl} />}
|
||||||
|
<div className="add-data-source-item-text-wrapper">
|
||||||
|
<span className="add-data-source-item-text">{title}</span>
|
||||||
|
{description && <span className="add-data-source-item-desc">{description}</span>}
|
||||||
|
</div>
|
||||||
|
{actions && <div className="add-data-source-item-actions">{actions}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Container, CustomScrollbar, ValuePicker } from '@grafana/ui';
|
import {
|
||||||
|
Container,
|
||||||
|
CustomScrollbar,
|
||||||
|
InfoBox,
|
||||||
|
ValuePicker,
|
||||||
|
Button,
|
||||||
|
useTheme,
|
||||||
|
VerticalGroup,
|
||||||
|
stylesFactory,
|
||||||
|
} from '@grafana/ui';
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
DataTransformerConfig,
|
DataTransformerConfig,
|
||||||
|
GrafanaTheme,
|
||||||
SelectableValue,
|
SelectableValue,
|
||||||
standardTransformersRegistry,
|
standardTransformersRegistry,
|
||||||
transformDataFrame,
|
transformDataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { TransformationOperationRow } from './TransformationOperationRow';
|
import { TransformationOperationRow } from './TransformationOperationRow';
|
||||||
|
import { Card, CardProps } from '../../../../core/components/Card/Card';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onChange: (transformations: DataTransformerConfig[]) => void;
|
onChange: (transformations: DataTransformerConfig[]) => void;
|
||||||
@@ -52,10 +64,10 @@ export class TransformationsEditor extends React.PureComponent<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ValuePicker
|
<ValuePicker
|
||||||
|
size="md"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
label="Add transformation"
|
label="Add transformation"
|
||||||
options={availableTransformers}
|
options={availableTransformers}
|
||||||
size="lg"
|
|
||||||
onChange={this.onTransformationAdd}
|
onChange={this.onTransformationAdd}
|
||||||
isFullWidth={false}
|
isFullWidth={false}
|
||||||
/>
|
/>
|
||||||
@@ -109,17 +121,54 @@ export class TransformationsEditor extends React.PureComponent<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const hasTransformationsConfigured = this.props.transformations.length > 0;
|
||||||
return (
|
return (
|
||||||
<CustomScrollbar autoHeightMin="100%">
|
<CustomScrollbar autoHeightMin="100%">
|
||||||
<Container padding="md">
|
<Container padding="md">
|
||||||
<p className="muted">
|
{!hasTransformationsConfigured && (
|
||||||
Transformations allow you to combine, re-order, hide and rename specific parts the the data set before being
|
<InfoBox>
|
||||||
visualized.
|
<p>
|
||||||
</p>
|
Transformations allow you to combine, re-order, hide and rename specific parts the the data set before
|
||||||
{this.renderTransformationEditors()}
|
being visualized. Choose one of the transformations below to start with:
|
||||||
{this.renderTransformationSelector()}
|
</p>
|
||||||
|
<VerticalGroup>
|
||||||
|
{standardTransformersRegistry.list().map(t => {
|
||||||
|
return (
|
||||||
|
<TransformationCard
|
||||||
|
title={t.name}
|
||||||
|
description={t.description}
|
||||||
|
actions={<Button>Select</Button>}
|
||||||
|
onClick={() => {
|
||||||
|
this.onTransformationAdd({ value: t.id });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</VerticalGroup>
|
||||||
|
</InfoBox>
|
||||||
|
)}
|
||||||
|
{hasTransformationsConfigured && this.renderTransformationEditors()}
|
||||||
|
{hasTransformationsConfigured && this.renderTransformationSelector()}
|
||||||
</Container>
|
</Container>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TransformationCard: React.FC<CardProps> = props => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getTransformationCardStyles(theme);
|
||||||
|
return <Card {...props} className={styles.card} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
return {
|
||||||
|
card: css`
|
||||||
|
background: ${theme.colors.bg2};
|
||||||
|
width: 100%;
|
||||||
|
&:hover {
|
||||||
|
background: ${theme.colors.bg3};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { FC, PureComponent } from 'react';
|
import React, { FC, PureComponent } from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { DataSourcePluginMeta, NavModel } from '@grafana/data';
|
import { DataSourcePluginMeta, NavModel } from '@grafana/data';
|
||||||
@@ -12,6 +11,7 @@ import { addDataSource, loadDataSourcePlugins } from './state/actions';
|
|||||||
import { getDataSourcePlugins } from './state/selectors';
|
import { getDataSourcePlugins } from './state/selectors';
|
||||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||||
import { setDataSourceTypeSearchQuery } from './state/reducers';
|
import { setDataSourceTypeSearchQuery } from './state/reducers';
|
||||||
|
import { Card } from 'app/core/components/Card/Card';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
@@ -120,37 +120,33 @@ const DataSourceTypeCard: FC<DataSourceTypeCardProps> = props => {
|
|||||||
|
|
||||||
// find first plugin info link
|
// find first plugin info link
|
||||||
const learnMoreLink = plugin.info.links && plugin.info.links.length > 0 ? plugin.info.links[0] : null;
|
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 (
|
return (
|
||||||
<div
|
<Card
|
||||||
className={mainClassName}
|
title={plugin.name}
|
||||||
|
description={plugin.info.description}
|
||||||
|
ariaLabel={e2e.pages.AddDataSource.selectors.dataSourcePlugins(plugin.name)}
|
||||||
|
logoUrl={plugin.info.logos.small}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
{learnMoreLink && (
|
||||||
|
<LinkButton
|
||||||
|
variant="secondary"
|
||||||
|
href={`${learnMoreLink.url}?utm_source=grafana_add_ds`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
onClick={onLearnMoreClick}
|
||||||
|
icon="external-link-alt"
|
||||||
|
>
|
||||||
|
{learnMoreLink.name}
|
||||||
|
</LinkButton>
|
||||||
|
)}
|
||||||
|
{!isPhantom && <Button>Select</Button>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
className={isPhantom && 'add-data-source-item--phantom'}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-label={e2e.pages.AddDataSource.selectors.dataSourcePlugins(plugin.name)}
|
/>
|
||||||
>
|
|
||||||
<img className="add-data-source-item-logo" src={plugin.info.logos.small} />
|
|
||||||
<div className="add-data-source-item-text-wrapper">
|
|
||||||
<span className="add-data-source-item-text">{plugin.name}</span>
|
|
||||||
{plugin.info.description && <span className="add-data-source-item-desc">{plugin.info.description}</span>}
|
|
||||||
</div>
|
|
||||||
<div className="add-data-source-item-actions">
|
|
||||||
{learnMoreLink && (
|
|
||||||
<LinkButton
|
|
||||||
variant="secondary"
|
|
||||||
href={`${learnMoreLink.url}?utm_source=grafana_add_ds`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
onClick={onLearnMoreClick}
|
|
||||||
icon="external-link-alt"
|
|
||||||
>
|
|
||||||
{learnMoreLink.name}
|
|
||||||
</LinkButton>
|
|
||||||
)}
|
|
||||||
{!isPhantom && <Button>Select</Button>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user