diff --git a/packages/grafana-ui/src/components/InfoBox/DismissableFeatureInfoBox.tsx b/packages/grafana-ui/src/components/InfoBox/DismissableFeatureInfoBox.tsx new file mode 100644 index 00000000000..d29dd6f1a5a --- /dev/null +++ b/packages/grafana-ui/src/components/InfoBox/DismissableFeatureInfoBox.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useLocalStorage } from 'react-use'; +import { FeatureInfoBox, FeatureInfoBoxProps } from './FeatureInfoBox'; + +export const FEATUREINFOBOX_PERSISTENCE_ID_PREFIX = 'grafana-ui.components.InfoBox.FeatureInfoBox'; + +export interface DismissableFeatureInfoBoxProps extends FeatureInfoBoxProps { + /** Unique id under which this instance will be persisted. */ + persistenceId: string; +} + +/** + @internal + Wraps FeatureInfoBox and perists if a user has dismissed the box in local storage. + */ +export const DismissableFeatureInfoBox = React.memo( + React.forwardRef( + ({ persistenceId, onDismiss, ...otherProps }, ref) => { + const localStorageKey = FEATUREINFOBOX_PERSISTENCE_ID_PREFIX.concat(persistenceId); + + const [dismissed, setDismissed] = useLocalStorage(localStorageKey, { isDismissed: false }); + + const dismiss = () => { + setDismissed({ isDismissed: true }); + if (onDismiss) { + onDismiss(); + } + }; + + if (dismissed.isDismissed) { + return null; + } + return ; + } + ) +); +DismissableFeatureInfoBox.displayName = 'DismissableFeatureInfoBox'; diff --git a/packages/grafana-ui/src/components/InfoBox/FeatureInfoBox.tsx b/packages/grafana-ui/src/components/InfoBox/FeatureInfoBox.tsx index b1bb5597b12..f49df4d93d6 100644 --- a/packages/grafana-ui/src/components/InfoBox/FeatureInfoBox.tsx +++ b/packages/grafana-ui/src/components/InfoBox/FeatureInfoBox.tsx @@ -1,19 +1,18 @@ import React from 'react'; import { InfoBox, InfoBoxProps } from './InfoBox'; import { FeatureState, GrafanaTheme } from '@grafana/data'; -import { stylesFactory, useTheme } from '../../themes'; +import { stylesFactory, useStyles } from '../../themes'; import { Badge, BadgeProps } from '../Badge/Badge'; import { css } from 'emotion'; -interface FeatureInfoBoxProps extends Omit { +export interface FeatureInfoBoxProps extends Omit { title: string; featureState?: FeatureState; } export const FeatureInfoBox = React.memo( React.forwardRef(({ title, featureState, ...otherProps }, ref) => { - const theme = useTheme(); - const styles = getFeatureInfoBoxStyles(theme); + const styles = useStyles(getFeatureInfoBoxStyles); const titleEl = featureState ? ( <> @@ -25,7 +24,7 @@ export const FeatureInfoBox = React.memo( ) : (

{title}

); - return ; + return ; }) ); FeatureInfoBox.displayName = 'FeatureInfoBox'; diff --git a/packages/grafana-ui/src/components/InfoBox/InfoBox.story.tsx b/packages/grafana-ui/src/components/InfoBox/InfoBox.story.tsx index d39ce8a5455..b7bbdc209d2 100644 --- a/packages/grafana-ui/src/components/InfoBox/InfoBox.story.tsx +++ b/packages/grafana-ui/src/components/InfoBox/InfoBox.story.tsx @@ -1,8 +1,17 @@ import React from 'react'; -import { number, select, text } from '@storybook/addon-knobs'; import { FeatureState } from '@grafana/data'; import { InfoBox, FeatureInfoBox } from '@grafana/ui'; import mdx from './InfoBox.mdx'; +import { + DismissableFeatureInfoBox, + DismissableFeatureInfoBoxProps, + FEATUREINFOBOX_PERSISTENCE_ID_PREFIX, +} from './DismissableFeatureInfoBox'; +import { Button } from '../Button'; +import { css } from 'emotion'; +import { Story } from '@storybook/react'; +import { FeatureInfoBoxProps } from './FeatureInfoBox'; +import { InfoBoxProps } from './InfoBox'; export default { title: 'Layout/InfoBox', @@ -13,67 +22,64 @@ export default { page: mdx, }, }, + argTypes: { + onDismiss: { action: 'Dismissed' }, + featureState: { + control: { type: 'select', options: ['alpha', 'beta', undefined] }, + }, + children: { + table: { + disable: true, + }, + }, + }, }; -const getKnobs = () => { - const containerWidth = number('Container width', 800, { - range: true, - min: 100, - max: 1500, - step: 100, - }); +const defaultProps: DismissableFeatureInfoBoxProps = { + title: 'A title', + severity: 'info', + url: 'http://www.grafana.com', + persistenceId: 'storybook-feature-info-box-persist', + featureState: FeatureState.beta, - const title = text('Title', 'User permission'); - const url = text('Url', 'http://docs.grafana.org/features/datasources/mysql/'); - const severity = select('Severity', ['success', 'warning', 'error', 'info'], 'info'); - - return { containerWidth, severity, title, url }; + children: ( +

+ The database user should only be granted SELECT permissions on the specified database & tables you want to + query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, + statements like USE otherdb; and DROP TABLE user; would be executed. To protect against + this we Highly recommend you create a specific MySQL user with restricted permissions. +

+ ), }; -export const basic = () => { - const { containerWidth, severity, title, url } = getKnobs(); +const InfoBoxTemplate: Story = (args) => ; +export const infoBox = InfoBoxTemplate.bind({}); +infoBox.args = defaultProps; + +const FeatureInfoBoxTemplate: Story = (args) => ; +export const featureInfoBox = FeatureInfoBoxTemplate.bind({}); +featureInfoBox.args = defaultProps; + +const DismissableTemplate: Story = (args) => { + const onResetClick = () => { + localStorage.removeItem(FEATUREINFOBOX_PERSISTENCE_ID_PREFIX.concat(args.persistenceId)); + location.reload(); + }; return ( -
- { - alert('onDismiss clicked'); - }} +
+
+ +
+
-

- The database user should only be granted SELECT permissions on the specified database & tables you want to - query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, - statements like USE otherdb; and DROP TABLE user; would be executed. To protect - against this we Highly recommend you create a specific MySQL user with restricted - permissions. -

- -
- ); -}; - -export const featureInfoBox = () => { - const { containerWidth } = getKnobs(); - - return ( -
- { - alert('onDismiss clicked'); - }} - > - Transformations allow you to join, calculate, re-order, hide and rename your query results before being - visualized.
- Many transforms are not suitable if you're using the Graph visualisation as it currently only supports time - series.
- It can help to switch to Table visualisation to understand what a transformation is doing. -
+ +
); }; +export const dismissableFeatureInfoBox = DismissableTemplate.bind({}); +dismissableFeatureInfoBox.args = defaultProps; diff --git a/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx b/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx index 1646f9fde51..6fefdd70f97 100644 --- a/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx +++ b/packages/grafana-ui/src/components/InfoBox/InfoBox.tsx @@ -34,7 +34,7 @@ export const InfoBox = React.memo( ({ title, className, children, branded, url, urlTitle, onDismiss, severity = 'info', ...otherProps }, ref) => { const theme = useTheme(); const styles = getInfoBoxStyles(theme, severity); - const wrapperClassName = branded ? cx(styles.wrapperBranded, className) : cx(styles.wrapper, className); + const wrapperClassName = cx(branded ? styles.wrapperBranded : styles.wrapper, className); return (
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 0b86c16b896..126587861b6 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -103,6 +103,7 @@ export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu'; export { SeriesIcon } from './VizLegend/SeriesIcon'; export { InfoBox } from './InfoBox/InfoBox'; export { FeatureBadge, FeatureInfoBox } from './InfoBox/FeatureInfoBox'; +export { DismissableFeatureInfoBox } from './InfoBox/DismissableFeatureInfoBox'; export { JSONFormatter } from './JSONFormatter/JSONFormatter'; export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer'; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx index 902ddf53d7c..191b42b6596 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationsEditor.tsx @@ -4,11 +4,13 @@ import { Button, Container, CustomScrollbar, - FeatureInfoBox, stylesFactory, + Themeable, + DismissableFeatureInfoBox, useTheme, ValuePicker, VerticalGroup, + withTheme, } from '@grafana/ui'; import { DataFrame, @@ -31,7 +33,7 @@ import { TransformationsEditorTransformation } from './types'; import { PanelNotSupported } from '../PanelEditor/PanelNotSupported'; import { AppNotificationSeverity } from '../../../../types'; -interface TransformationsEditorProps { +interface TransformationsEditorProps extends Themeable { panel: PanelModel; } @@ -40,7 +42,7 @@ interface State { transformations: TransformationsEditorTransformation[]; } -export class TransformationsEditor extends React.PureComponent { +class UnThemedTransformationsEditor extends React.PureComponent { subscription?: Unsubscribable; constructor(props: TransformationsEditorProps) { @@ -208,9 +210,16 @@ export class TransformationsEditor extends React.PureComponent + <> - +

Transformations allow you to join, calculate, re-order, hide and rename your query results before being visualized.
@@ -218,7 +227,7 @@ export class TransformationsEditor extends React.PureComponent It can help to switch to Table visualization to understand what a transformation is doing.

-
+
{standardTransformersRegistry.list().map((t) => { @@ -236,7 +245,7 @@ export class TransformationsEditor extends React.PureComponent - + ); } @@ -299,3 +308,5 @@ const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => { `, }; }); + +export const TransformationsEditor = withTheme(UnThemedTransformationsEditor);