Transforms: Adds beta notice and updates transform descriptions (#24158)

* Transforms: Adds beta notice and updates transform descriptions

* Rename organize fields

* Webpack - enable images import

* Introduce FeatureState type

* Alow Container component grow/shrink config

* Enable svg import in main app

* Jest + webpack for svgs

* InfoBox refactor (+ added feature info box), Badge component introduced

* Update packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx

Co-authored-by: Carl Bergquist <carl@grafana.com>

* Minor fixes

* Update packages/grafana-ui/src/components/TransformersUI/OrganizeFieldsTransformerEditor.tsx

Co-authored-by: Carl Bergquist <carl@grafana.com>

* Update packages/grafana-ui/src/components/TransformersUI/SeriesToFieldsTransformerEditor.tsx

Co-authored-by: Carl Bergquist <carl@grafana.com>

* fix typo

* Build storybook fixed

* Fix padding

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: Carl Bergquist <carl@grafana.com>
pull/24235/head
Torkel Ödegaard 5 years ago committed by GitHub
parent 0fe9e7e242
commit 92a16d2e10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      jest.config.js
  2. 2
      packages/grafana-data/src/transformations/transformers/reduce.ts
  3. 9
      packages/grafana-data/src/types/app.ts
  4. 9
      packages/grafana-data/src/utils/docs.ts
  5. 1
      packages/grafana-data/src/utils/index.ts
  6. 7
      packages/grafana-runtime/tsconfig.json
  7. 7
      packages/grafana-ui/.storybook/tsconfig.json
  8. 35
      packages/grafana-ui/src/components/Badge/Badge.story.tsx
  9. 90
      packages/grafana-ui/src/components/Badge/Badge.tsx
  10. 63
      packages/grafana-ui/src/components/InfoBox/FeatureInfoBox.tsx
  11. 40
      packages/grafana-ui/src/components/InfoBox/InfoBox.story.tsx
  12. 92
      packages/grafana-ui/src/components/InfoBox/InfoBox.tsx
  13. 58
      packages/grafana-ui/src/components/InfoBox/panelArt_dark.svg
  14. 64
      packages/grafana-ui/src/components/InfoBox/panelArt_light.svg
  15. 24
      packages/grafana-ui/src/components/Layout/Layout.tsx
  16. 2
      packages/grafana-ui/src/components/TransformersUI/FilterByNameTransformerEditor.tsx
  17. 5
      packages/grafana-ui/src/components/TransformersUI/FilterByRefIdTransformerEditor.tsx
  18. 3
      packages/grafana-ui/src/components/TransformersUI/LabelsToFieldsTransformerEditor.tsx
  19. 5
      packages/grafana-ui/src/components/TransformersUI/OrganizeFieldsTransformerEditor.tsx
  20. 5
      packages/grafana-ui/src/components/TransformersUI/SeriesToFieldsTransformerEditor.tsx
  21. 2
      packages/grafana-ui/src/components/index.ts
  22. 2
      packages/grafana-ui/tsconfig.json
  23. 10
      public/app/core/utils/docsLinks.ts
  24. 19
      public/app/features/dashboard/components/PanelEditor/FieldConfigEditor.tsx
  25. 43
      public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx
  26. 36
      public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx
  27. 119
      public/app/features/plugins/PluginSignatureBadge.tsx
  28. 4
      public/app/types/svg.d.ts
  29. 1
      public/test/mocks/svg.ts
  30. 5
      scripts/webpack/webpack.common.js
  31. 4
      scripts/webpack/webpack.dev.js

@ -14,4 +14,7 @@ module.exports = {
setupFiles: ['jest-canvas-mock', './public/test/jest-shim.ts', './public/test/jest-setup.ts'],
snapshotSerializers: ['enzyme-to-json/serializer'],
globals: { 'ts-jest': { isolatedModules: true } },
moduleNameMapper: {
'\\.svg': '<rootDir>/public/test/mocks/svg.ts',
},
};

@ -18,7 +18,7 @@ export interface ReduceTransformerOptions {
export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> = {
id: DataTransformerID.reduce,
name: 'Reduce',
description: 'Reduce all rows to a single row and concatenate all results',
description: 'Reduce all rows or data points to a single value using a function like max, min, mean or last',
defaultOptions: {
reducers: [ReducerID.max],
},

@ -74,3 +74,12 @@ export class AppPlugin<T = KeyValue> extends GrafanaPlugin<AppPluginMeta<T>> {
}
}
}
/**
* Defines life cycle of a feature
* @internal
*/
export enum FeatureState {
alpha = 'alpha',
beta = 'beta',
}

@ -0,0 +1,9 @@
/**
* Enumeration of documentation topics
* @internal
*/
export enum DocsId {
Transformations,
FieldConfig,
FieldConfigOverrides,
}

@ -16,3 +16,4 @@ export { getFlotPairs, getFlotPairsConstant } from './flotPairs';
export { locationUtil } from './location';
export { urlUtil, UrlQueryMap, UrlQueryValue } from './url';
export { DataLinkBuiltInVars } from './dataLinks';
export { DocsId } from './docs';

@ -11,5 +11,10 @@
},
"exclude": ["dist", "node_modules"],
"extends": "@grafana/tsconfig",
"include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts", "../../public/app/types/sanitize-url.d.ts"]
"include": [
"src/**/*.ts*",
"../../public/app/types/jquery/*.ts",
"../../public/app/types/sanitize-url.d.ts",
"../../public/app/types/svg.d.ts"
]
}

@ -6,5 +6,10 @@
},
"exclude": ["../dist", "../node_modules"],
"extends": "../tsconfig.json",
"include": ["../src/**/*.ts", "../src/**/*.tsx", "../../../public/app/types/sanitize-url.d.ts"]
"include": [
"../src/**/*.ts",
"../src/**/*.tsx",
"../../../public/app/types/sanitize-url.d.ts",
"../../../public/app/types/svg.d.ts"
]
}

@ -0,0 +1,35 @@
import React from 'react';
import { boolean, text, select } from '@storybook/addon-knobs';
import { Badge, BadgeColor } from './Badge';
export default {
title: 'Other/Badge',
component: Badge,
decorators: [],
parameters: {
docs: {},
},
};
export const basic = () => {
const badgeColor = select<BadgeColor>(
'Badge color',
{
Red: 'red',
Green: 'green',
Blue: 'blue',
Orange: 'orange',
},
'blue'
);
const withIcon = boolean('With icon', true);
const tooltipText = text('Tooltip text', '');
return (
<Badge
text={'Badge label'}
color={badgeColor}
icon={withIcon ? 'rocket' : undefined}
tooltip={tooltipText.trim() === '' ? undefined : tooltipText}
/>
);
};

@ -0,0 +1,90 @@
import React from 'react';
import { Icon } from '../Icon/Icon';
import { useTheme } from '../../themes/ThemeContext';
import { stylesFactory } from '../../themes/stylesFactory';
import { IconName } from '../../types';
import { Tooltip } from '../Tooltip/Tooltip';
import { getColorFromHexRgbOrName, GrafanaTheme } from '@grafana/data';
import tinycolor from 'tinycolor2';
import { css } from 'emotion';
import { HorizontalGroup } from '..';
export type BadgeColor = 'blue' | 'red' | 'green' | 'orange';
export interface BadgeProps {
text: string;
color: BadgeColor;
icon?: IconName;
tooltip?: string;
}
export const Badge = React.memo<BadgeProps>(({ icon, color, text, tooltip }) => {
const theme = useTheme();
const styles = getStyles(theme, color);
const badge = (
<div className={styles.wrapper}>
<HorizontalGroup align="center" spacing="xs">
{icon && <Icon name={icon} size="sm" />}
<span>{text}</span>
</HorizontalGroup>
</div>
);
return tooltip ? (
<Tooltip content={tooltip} placement="auto">
{badge}
</Tooltip>
) : (
badge
);
});
Badge.displayName = 'Badge';
const getStyles = stylesFactory((theme: GrafanaTheme, color: BadgeColor) => {
let sourceColor = getColorFromHexRgbOrName(color);
let borderColor = '';
let bgColor = '';
let textColor = '';
if (theme.isDark) {
bgColor = tinycolor(sourceColor)
.darken(38)
.toString();
borderColor = tinycolor(sourceColor)
.darken(25)
.toString();
textColor = tinycolor(sourceColor)
.lighten(45)
.toString();
} else {
bgColor = tinycolor(sourceColor)
.lighten(30)
.toString();
borderColor = tinycolor(sourceColor)
.lighten(15)
.toString();
textColor = tinycolor(sourceColor)
.darken(40)
.toString();
}
return {
wrapper: css`
font-size: ${theme.typography.size.sm};
display: inline-flex;
padding: 1px 4px;
border-radius: 3px;
margin-top: 6px;
background: ${bgColor};
border: 1px solid ${borderColor};
color: ${textColor};
> span {
position: relative;
top: 1px;
margin-left: 2px;
}
`,
};
});

@ -0,0 +1,63 @@
import React from 'react';
import { InfoBox, InfoBoxProps } from './InfoBox';
import { FeatureState, GrafanaTheme } from '@grafana/data';
import { stylesFactory, useTheme } from '../../themes';
import { Badge, BadgeProps } from '../Badge/Badge';
import { css } from 'emotion';
interface FeatureInfoBox extends Omit<InfoBoxProps, 'branded' | 'title' | 'urlTitle'> {
title: string;
featureState?: FeatureState;
}
export const FeatureInfoBox = React.memo(
React.forwardRef<HTMLDivElement, FeatureInfoBox>(({ title, featureState, ...otherProps }, ref) => {
const theme = useTheme();
const styles = getFeatureInfoBoxStyles(theme);
const titleEl = featureState ? (
<>
<div className={styles.badge}>
<FeatureBadge featureState={featureState} />
</div>
<h3>{title}</h3>
</>
) : (
<h3>{title}</h3>
);
return <InfoBox branded title={titleEl} urlTitle="Read documentation" {...otherProps} />;
})
);
const getFeatureInfoBoxStyles = stylesFactory((theme: GrafanaTheme) => {
return {
badge: css`
margin-bottom: ${theme.spacing.sm};
`,
};
});
interface FeatureBadgeProps {
featureState: FeatureState;
}
export const FeatureBadge: React.FC<FeatureBadgeProps> = ({ featureState }) => {
const display = getPanelStateBadgeDisplayModel(featureState);
return <Badge text={display.text} color={display.color} icon={display.icon} />;
};
function getPanelStateBadgeDisplayModel(featureState: FeatureState): BadgeProps {
switch (featureState) {
case FeatureState.alpha:
return {
text: 'Alpha',
icon: 'exclamation-triangle',
color: 'orange',
};
}
return {
text: 'Beta',
icon: 'rocket',
color: 'blue',
};
}

@ -1,6 +1,8 @@
import React from 'react';
import { number } from '@storybook/addon-knobs';
import { InfoBox } from './InfoBox';
import { FeatureInfoBox } from './FeatureInfoBox';
import { FeatureState } from '@grafana/data';
export default {
title: 'Layout/InfoBox',
@ -35,16 +37,11 @@ export const basic = () => {
return (
<div style={{ width: containerWidth }}>
<InfoBox
header="User Permission"
footer={
<>
Checkout the{' '}
<a className="external-link" target="_blank" href="http://docs.grafana.org/features/datasources/mysql/">
MySQL Data Source Docs
</a>{' '}
for more information.,
</>
}
title="User Permission"
url={'http://docs.grafana.org/features/datasources/mysql/'}
onDismiss={() => {
alert('onDismiss clicked');
}}
>
<p>
The database user should only be granted SELECT permissions on the specified database &amp; tables you want to
@ -57,3 +54,26 @@ export const basic = () => {
</div>
);
};
export const featureInfoBox = () => {
const { containerWidth } = getKnobs();
return (
<div style={{ width: containerWidth }}>
<FeatureInfoBox
title="Transformations"
url={'http://www.grafana.com'}
featureState={FeatureState.beta}
onDismiss={() => {
alert('onDismiss clicked');
}}
>
Transformations allow you to join, calculate, re-order, hide and rename your query results before being
visualized. <br />
Many transforms are not suitable if your using the Graph visualisation as it currently only supports time
series. <br />
It can help to switch to Table visualisation to understand what a transformation is doing.
</FeatureInfoBox>
</div>
);
};

@ -2,10 +2,19 @@ import React from 'react';
import { css, cx } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { stylesFactory, useTheme } from '../../themes';
import { Icon } from '../Icon/Icon';
import { IconButton } from '../IconButton/IconButton';
import { HorizontalGroup } from '../Layout/Layout';
import panelArtDark from './panelArt_dark.svg';
import panelArtLight from './panelArt_light.svg';
export interface Props extends React.HTMLAttributes<HTMLDivElement> {
header?: string | JSX.Element;
footer?: string | JSX.Element;
export interface InfoBoxProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
children: React.ReactNode;
title?: string | JSX.Element;
url?: string;
urlTitle?: string;
branded?: boolean;
onDismiss?: () => void;
}
/**
@ -14,33 +23,39 @@ export interface Props extends React.HTMLAttributes<HTMLDivElement> {
* @Alpha
*/
export const InfoBox = React.memo(
React.forwardRef<HTMLDivElement, Props>(({ header, footer, className, children, ...otherProps }, ref) => {
const theme = useTheme();
const css = getInfoBoxStyles(theme);
React.forwardRef<HTMLDivElement, InfoBoxProps>(
({ title, className, children, branded, url, urlTitle, onDismiss, ...otherProps }, ref) => {
const theme = useTheme();
const styles = getInfoBoxStyles(theme);
const wrapperClassName = branded ? cx(styles.wrapperBranded, className) : cx(styles.wrapper, className);
return (
<div className={cx([css.wrapper, className])} {...otherProps} ref={ref}>
{header && (
<div className={css.header}>
<h5>{header}</h5>
return (
<div className={wrapperClassName} {...otherProps} ref={ref}>
<div>
<HorizontalGroup justify={'space-between'} align={'flex-start'}>
<div>{typeof title === 'string' ? <h4>{title}</h4> : title}</div>
{onDismiss && <IconButton name={'times'} onClick={onDismiss} />}
</HorizontalGroup>
</div>
)}
{children}
{footer && <div className={css.footer}>{footer}</div>}
</div>
);
})
<div>{children}</div>
{url && (
<a href={url} className={styles.docsLink} target="_blank">
<Icon name="book" /> {urlTitle || 'Read more'}
</a>
)}
</div>
);
}
)
);
const getInfoBoxStyles = stylesFactory((theme: GrafanaTheme) => ({
wrapper: css`
position: relative;
padding: ${theme.spacing.lg};
padding: ${theme.spacing.md};
background-color: ${theme.colors.bg2};
border-top: 3px solid ${theme.palette.blue80};
margin-bottom: ${theme.spacing.md};
margin-right: ${theme.spacing.xs};
box-shadow: ${theme.shadows.listItem};
flex-grow: 1;
ul {
@ -60,18 +75,39 @@ const getInfoBoxStyles = stylesFactory((theme: GrafanaTheme) => ({
margin-bottom: 0;
}
a {
@extend .external-link;
}
&--max-lg {
max-width: ${theme.breakpoints.lg};
}
`,
header: css`
margin-bottom: ${theme.spacing.d};
wrapperBranded: css`
padding: ${theme.spacing.md};
border-radius: ${theme.border.radius.md};
position: relative;
box-shadow: 0 0 30px 10px rgba(0, 0, 0, ${theme.isLight ? 0.05 : 0.2});
z-index: 0;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url(${theme.isLight ? panelArtLight : panelArtDark});
border-radius: ${theme.border.radius.md};
background-position: 50% 50%;
background-size: cover;
filter: saturate(80%);
z-index: -1;
}
p:last-child {
margin-bottom: 0;
}
`,
footer: css`
margin-top: ${theme.spacing.d};
docsLink: css`
display: inline-block;
margin-top: ${theme.spacing.lg};
font-size: ${theme.typography.size.sm};
`,
}));

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

@ -1,5 +1,5 @@
import React, { HTMLProps } from 'react';
import { css } from 'emotion';
import { css, cx } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { stylesFactory, useTheme } from '../../themes';
@ -24,6 +24,8 @@ export interface LayoutProps extends Omit<HTMLProps<HTMLDivElement>, 'align' | '
export interface ContainerProps {
padding?: Spacing;
margin?: Spacing;
grow?: number;
shrink?: number;
}
export const Layout: React.FC<LayoutProps> = ({
@ -84,10 +86,26 @@ export const VerticalGroup: React.FC<Omit<LayoutProps, 'orientation' | 'wrap'>>
</Layout>
);
export const Container: React.FC<ContainerProps> = ({ children, padding, margin }) => {
export const Container: React.FC<ContainerProps> = ({ children, padding, margin, grow, shrink }) => {
const theme = useTheme();
const styles = getContainerStyles(theme, padding, margin);
return <div className={styles.wrapper}>{children}</div>;
return (
<div
className={cx(
styles.wrapper,
grow !== undefined &&
css`
flex-grow: ${grow};
`,
shrink !== undefined &&
css`
flex-shrink: ${shrink};
`
)}
>
{children}
</div>
);
};
const getStyles = stylesFactory(

@ -190,5 +190,5 @@ export const filterFieldsByNameTransformRegistryItem: TransformerRegistyItem<Fil
editor: FilterByNameTransformerEditor,
transformation: standardTransformers.filterFieldsByNameTransformer,
name: 'Filter by name',
description: 'Filter fields by name',
description: 'Removes part of the query results using a regex pattern. The pattern can be inclusive or exclusive.',
};

@ -133,6 +133,7 @@ export const filterFramesByRefIdTransformRegistryItem: TransformerRegistyItem<Fi
id: DataTransformerID.filterByRefId,
editor: FilterByRefIdTransformerEditor,
transformation: standardTransformers.filterFramesByRefIdTransformer,
name: 'Filter by refId',
description: 'Filter results by refId',
name: 'Filter data by query',
description:
'Filter data by query. This is useful if you are sharing the results from a different panel that has many queries and you want to only visualize a subset of that in this panel.',
};

@ -15,5 +15,6 @@ export const labelsToFieldsTransformerRegistryItem: TransformerRegistyItem<Label
editor: LabelsAsFieldsTransformerEditor,
transformation: standardTransformers.labelsToFieldsTransformer,
name: 'Labels to fields',
description: 'Groups series by time and return labels or tags as fields',
description: `Groups series by time and return labels or tags as fields.
Useful for showing time series with labels in a table where each label key becomes a seperate column`,
};

@ -221,6 +221,7 @@ export const organizeFieldsTransformRegistryItem: TransformerRegistyItem<Organiz
id: DataTransformerID.organize,
editor: OrganizeFieldsTransformerEditor,
transformation: standardTransformers.organizeFieldsTransformer,
name: 'Organize fields',
description: 'Order, filter and rename fields',
name: 'Change order, hide and rename',
description:
"Allows the user to re-order, hide, or rename columns. Useful when data source doesn't allow overrides for visualizing data.",
};

@ -42,6 +42,7 @@ export const seriesToFieldsTransformerRegistryItem: TransformerRegistyItem<Serie
id: DataTransformerID.seriesToColumns,
editor: SeriesToFieldsTransformerEditor,
transformation: standardTransformers.seriesToColumnsTransformer,
name: 'Join by field',
description: 'Joins many time series / data frames by a field',
name: 'Outer join',
description:
'Joins many time series/tables by a field. This can be used to outer join multiple time series on the _time_ field to show many time series in one table.',
};

@ -99,6 +99,7 @@ export { DataLinkInput } from './DataLinks/DataLinkInput';
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
export { SeriesIcon } from './Legend/SeriesIcon';
export { InfoBox } from './InfoBox/InfoBox';
export { FeatureInfoBox } from './InfoBox/FeatureInfoBox';
export { JSONFormatter } from './JSONFormatter/JSONFormatter';
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
@ -138,6 +139,7 @@ export * from './Select/Select';
export { ButtonSelect } from './Select/ButtonSelect';
export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout';
export { Badge, BadgeColor, BadgeProps } from './Badge/Badge';
export { RadioButtonGroup } from './Forms/RadioButtonGroup/RadioButtonGroup';
export { Input } from './Input/Input';

@ -11,5 +11,5 @@
},
"exclude": ["dist", "node_modules"],
"extends": "@grafana/tsconfig",
"include": ["src/**/*.ts*", "../../public/app/types/sanitize-url.d.ts"]
"include": ["src/**/*.ts*", "../../public/app/types/sanitize-url.d.ts", "../../public/app/types/svg.d.ts"]
}

@ -0,0 +1,10 @@
import { DocsId } from '@grafana/data';
// TODO: Documentation links
const DOCS_LINKS: Record<DocsId, string> = {
[DocsId.Transformations]: 'https://docs.grafana.com',
[DocsId.FieldConfig]: 'https://docs.grafana.com',
[DocsId.FieldConfigOverrides]: 'https://docs.grafana.com',
};
export const getDocsLink = (id: DocsId) => DOCS_LINKS[id];

@ -2,18 +2,20 @@ import React, { useCallback } from 'react';
import cloneDeep from 'lodash/cloneDeep';
import {
DataFrame,
FeatureState,
FieldConfigPropertyItem,
FieldConfigSource,
PanelPlugin,
SelectableValue,
VariableSuggestionsScope,
} from '@grafana/data';
import { Container, Counter, Field, fieldMatchersUI, Label, ValuePicker } from '@grafana/ui';
import { Container, Counter, FeatureInfoBox, Field, fieldMatchersUI, Label, useTheme, ValuePicker } from '@grafana/ui';
import { getDataLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
import { OverrideEditor } from './OverrideEditor';
import groupBy from 'lodash/groupBy';
import { OptionsGroup } from './OptionsGroup';
import { selectors } from '@grafana/e2e-selectors';
import { css } from 'emotion';
interface Props {
plugin: PanelPlugin;
@ -27,6 +29,8 @@ interface Props {
* Expects the container div to have size set and will fill it 100%
*/
export const OverrideFieldConfigEditor: React.FC<Props> = props => {
const theme = useTheme();
const { config } = props;
const onOverrideChange = (index: number, override: any) => {
const { config } = props;
let overrides = cloneDeep(config.overrides);
@ -104,6 +108,19 @@ export const OverrideFieldConfigEditor: React.FC<Props> = props => {
return (
<div aria-label={selectors.components.OverridesConfigEditor.content}>
{config.overrides.length === 0 && (
<FeatureInfoBox
title="Overrides"
featureState={FeatureState.beta}
// url={getDocsLink(DocsId.FieldConfigOverrides)}
className={css`
margin: ${theme.spacing.md};
`}
>
Field options overrides give you a fine grained control over how your data is displayed.
</FeatureInfoBox>
)}
{renderOverrides()}
{renderAddOverride()}
</div>

@ -1,8 +1,18 @@
import React from 'react';
import { Button, Container, CustomScrollbar, stylesFactory, useTheme, ValuePicker, VerticalGroup } from '@grafana/ui';
import {
Button,
Container,
CustomScrollbar,
FeatureInfoBox,
stylesFactory,
useTheme,
ValuePicker,
VerticalGroup,
} from '@grafana/ui';
import {
DataFrame,
DataTransformerConfig,
FeatureState,
GrafanaTheme,
SelectableValue,
standardTransformersRegistry,
@ -114,13 +124,23 @@ export class TransformationsEditor extends React.PureComponent<Props> {
renderNoAddedTransformsState() {
return (
<>
<p className="muted">
Transformations allow you to combine, re-order, hide and rename specific parts the the data set before being
visualized. <br />
Choose one of the transformations below to start with:
</p>
<VerticalGroup spacing={'lg'}>
<Container grow={1}>
<FeatureInfoBox
title="Transformations"
featureState={FeatureState.beta}
// 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 your using the Graph visualisation as it currently only supports time
series. <br />
It can help to switch to Table visualisation to understand what a transformation is doing. <br />
</p>
<p>Select one of the transformations below to start.</p>
</FeatureInfoBox>
</Container>
<VerticalGroup>
{standardTransformersRegistry.list().map(t => {
return (
@ -136,7 +156,7 @@ export class TransformationsEditor extends React.PureComponent<Props> {
);
})}
</VerticalGroup>
</>
</VerticalGroup>
);
}
@ -170,6 +190,11 @@ const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => {
border: none;
padding: ${theme.spacing.sm};
// hack because these cards use classes from a very different card for some reason
.add-data-source-item-text {
font-size: ${theme.typography.size.md};
}
&:hover {
background: ${theme.colors.bg3};
box-shadow: none;

@ -1,9 +1,8 @@
import React from 'react';
import { GrafanaTheme, PanelPluginMeta, PluginState } from '@grafana/data';
import { styleMixins, stylesFactory, useTheme } from '@grafana/ui';
import { Badge, BadgeProps, styleMixins, stylesFactory, useTheme } from '@grafana/ui';
import { css, cx } from 'emotion';
import { selectors } from '@grafana/e2e-selectors';
import { PanelPluginBadge } from '../../plugins/PluginSignatureBadge';
interface Props {
isCurrent: boolean;
@ -126,3 +125,36 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
});
export default VizTypePickerPlugin;
interface PanelPluginBadgeProps {
plugin: PanelPluginMeta;
}
const PanelPluginBadge: React.FC<PanelPluginBadgeProps> = ({ plugin }) => {
const display = getPanelStateBadgeDisplayModel(plugin);
if (plugin.state !== PluginState.deprecated && plugin.state !== PluginState.alpha) {
return null;
}
return <Badge color={display.color} text={display.text} icon={display.icon} tooltip={display.tooltip} />;
};
function getPanelStateBadgeDisplayModel(panel: PanelPluginMeta): BadgeProps {
switch (panel.state) {
case PluginState.deprecated:
return {
text: 'Deprecated',
icon: 'exclamation-triangle',
color: 'red',
tooltip: `${panel.name} panel is deprecated`,
};
}
return {
text: 'Alpha',
icon: 'rocket',
color: 'blue',
tooltip: `${panel.name} panel is experimental`,
};
}
PanelPluginBadge.displayName = 'PanelPluginBadge';

@ -1,61 +1,17 @@
import React from 'react';
import { Icon, IconName, stylesFactory, Tooltip, useTheme } from '@grafana/ui';
import {
getColorFromHexRgbOrName,
GrafanaTheme,
PanelPluginMeta,
PluginSignatureStatus,
PluginState,
} from '@grafana/data';
import { css } from 'emotion';
import tinycolor from 'tinycolor2';
import { Badge, BadgeProps } from '@grafana/ui';
import { PluginSignatureStatus } from '@grafana/data';
interface Props {
status: PluginSignatureStatus;
}
export const PluginSignatureBadge: React.FC<Props> = ({ status }) => {
const theme = useTheme();
const display = getSignatureDisplayModel(status);
const styles = getStyles(theme, display);
return (
<Tooltip content={display.tooltip} placement="left">
<div className={styles.wrapper}>
<Icon name={display.icon} size="sm" />
<span>{display.text}</span>
</div>
</Tooltip>
);
};
interface PanelPluginBadgeProps {
plugin: PanelPluginMeta;
}
export const PanelPluginBadge: React.FC<PanelPluginBadgeProps> = ({ plugin }) => {
const theme = useTheme();
const display = getPanelStateBadgeDisplayModel(plugin);
const styles = getStyles(theme, display);
if (plugin.state !== PluginState.deprecated && plugin.state !== PluginState.alpha) {
return null;
}
return (
<div className={styles.wrapper}>
<Icon name={display.icon} size="sm" />
<span>{display.text}</span>
</div>
);
return <Badge text={display.text} color={display.color} icon={display.icon} tooltip={display.tooltip} />;
};
interface DisplayModel {
text: string;
icon: IconName;
color: string;
tooltip: string;
}
function getSignatureDisplayModel(signature: PluginSignatureStatus): DisplayModel {
function getSignatureDisplayModel(signature: PluginSignatureStatus): BadgeProps {
switch (signature) {
case PluginSignatureStatus.internal:
return { text: 'Core', icon: 'cube', color: 'blue', tooltip: 'Core plugin that is bundled with Grafana' };
@ -80,71 +36,4 @@ function getSignatureDisplayModel(signature: PluginSignatureStatus): DisplayMode
return { text: 'Unsigned', icon: 'exclamation-triangle', color: 'red', tooltip: 'Unsigned external plugin' };
}
function getPanelStateBadgeDisplayModel(panel: PanelPluginMeta): DisplayModel {
switch (panel.state) {
case PluginState.deprecated:
return {
text: 'Deprecated',
icon: 'exclamation-triangle',
color: 'red',
tooltip: `${panel.name} panel is deprecated`,
};
}
return {
text: 'Alpha',
icon: 'rocket',
color: 'blue',
tooltip: `${panel.name} panel is experimental`,
};
}
const getStyles = stylesFactory((theme: GrafanaTheme, model: DisplayModel) => {
let sourceColor = getColorFromHexRgbOrName(model.color);
let borderColor = '';
let bgColor = '';
let textColor = '';
if (theme.isDark) {
bgColor = tinycolor(sourceColor)
.darken(38)
.toString();
borderColor = tinycolor(sourceColor)
.darken(25)
.toString();
textColor = tinycolor(sourceColor)
.lighten(45)
.toString();
} else {
bgColor = tinycolor(sourceColor)
.lighten(30)
.toString();
borderColor = tinycolor(sourceColor)
.lighten(15)
.toString();
textColor = tinycolor(sourceColor)
.darken(40)
.toString();
}
return {
wrapper: css`
font-size: ${theme.typography.size.sm};
display: inline-flex;
padding: 1px 4px;
border-radius: 3px;
margin-top: 6px;
background: ${bgColor};
border: 1px solid ${borderColor};
color: ${textColor};
> span {
position: relative;
top: 1px;
margin-left: 2px;
}
`,
};
});
PluginSignatureBadge.displayName = 'PluginSignatureBadge';

@ -0,0 +1,4 @@
declare module '*.svg' {
const content: string;
export default content;
}

@ -0,0 +1 @@
export const svg = 'svg';

@ -108,6 +108,11 @@ module.exports = {
},
],
},
{
test: /\.(svg|ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/,
loader: 'file-loader',
options: { name: 'static/img/[name].[hash:8].[ext]' },
},
],
},
// https://webpack.js.org/plugins/split-chunks-plugin/#split-chunks-example-3

@ -86,10 +86,6 @@ module.exports = (env = {}) =>
sourceMap: false,
preserveUrl: false,
}),
{
test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
loader: 'file-loader',
},
],
},

Loading…
Cancel
Save