Commit 99acad44 by Oscar Kilhed Committed by GitHub

GrafanaUI: Add a way to persistently close InfoBox (#30716)

* GrafanaUI: Add a way to persistently close InfoBox

InfoBox and FeatureInfoBox can take up a lot of screen realestate. This makes it easy to let the user close the boxes.

* Migrate InfoBox story to controls
parent 7a4c32d7
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<HTMLDivElement, DismissableFeatureInfoBoxProps>(
({ 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 <FeatureInfoBox onDismiss={dismiss} ref={ref} {...otherProps}></FeatureInfoBox>;
}
)
);
DismissableFeatureInfoBox.displayName = 'DismissableFeatureInfoBox';
import React from 'react'; import React from 'react';
import { InfoBox, InfoBoxProps } from './InfoBox'; import { InfoBox, InfoBoxProps } from './InfoBox';
import { FeatureState, GrafanaTheme } from '@grafana/data'; import { FeatureState, GrafanaTheme } from '@grafana/data';
import { stylesFactory, useTheme } from '../../themes'; import { stylesFactory, useStyles } from '../../themes';
import { Badge, BadgeProps } from '../Badge/Badge'; import { Badge, BadgeProps } from '../Badge/Badge';
import { css } from 'emotion'; import { css } from 'emotion';
interface FeatureInfoBoxProps extends Omit<InfoBoxProps, 'branded' | 'title' | 'urlTitle'> { export interface FeatureInfoBoxProps extends Omit<InfoBoxProps, 'branded' | 'title' | 'urlTitle'> {
title: string; title: string;
featureState?: FeatureState; featureState?: FeatureState;
} }
export const FeatureInfoBox = React.memo( export const FeatureInfoBox = React.memo(
React.forwardRef<HTMLDivElement, FeatureInfoBoxProps>(({ title, featureState, ...otherProps }, ref) => { React.forwardRef<HTMLDivElement, FeatureInfoBoxProps>(({ title, featureState, ...otherProps }, ref) => {
const theme = useTheme(); const styles = useStyles(getFeatureInfoBoxStyles);
const styles = getFeatureInfoBoxStyles(theme);
const titleEl = featureState ? ( const titleEl = featureState ? (
<> <>
...@@ -25,7 +24,7 @@ export const FeatureInfoBox = React.memo( ...@@ -25,7 +24,7 @@ export const FeatureInfoBox = React.memo(
) : ( ) : (
<h3>{title}</h3> <h3>{title}</h3>
); );
return <InfoBox branded title={titleEl} urlTitle="Read documentation" {...otherProps} />; return <InfoBox branded title={titleEl} urlTitle="Read documentation" ref={ref} {...otherProps} />;
}) })
); );
FeatureInfoBox.displayName = 'FeatureInfoBox'; FeatureInfoBox.displayName = 'FeatureInfoBox';
......
import React from 'react'; import React from 'react';
import { number, select, text } from '@storybook/addon-knobs';
import { FeatureState } from '@grafana/data'; import { FeatureState } from '@grafana/data';
import { InfoBox, FeatureInfoBox } from '@grafana/ui'; import { InfoBox, FeatureInfoBox } from '@grafana/ui';
import mdx from './InfoBox.mdx'; 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 { export default {
title: 'Layout/InfoBox', title: 'Layout/InfoBox',
...@@ -13,67 +22,64 @@ export default { ...@@ -13,67 +22,64 @@ export default {
page: mdx, page: mdx,
}, },
}, },
argTypes: {
onDismiss: { action: 'Dismissed' },
featureState: {
control: { type: 'select', options: ['alpha', 'beta', undefined] },
},
children: {
table: {
disable: true,
},
},
},
}; };
const getKnobs = () => { const defaultProps: DismissableFeatureInfoBoxProps = {
const containerWidth = number('Container width', 800, { title: 'A title',
range: true, severity: 'info',
min: 100, url: 'http://www.grafana.com',
max: 1500, persistenceId: 'storybook-feature-info-box-persist',
step: 100, 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 };
};
export const basic = () => {
const { containerWidth, severity, title, url } = getKnobs();
return ( children: (
<div style={{ width: containerWidth }}>
<InfoBox
title={title}
url={url}
severity={severity}
onDismiss={() => {
alert('onDismiss clicked');
}}
>
<p> <p>
The database user should only be granted SELECT permissions on the specified database &amp; tables you want to The database user should only be granted SELECT permissions on the specified database &amp; tables you want to
query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example, query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example,
statements like <code>USE otherdb;</code> and <code>DROP TABLE user;</code> would be executed. To protect statements like <code>USE otherdb;</code> and <code>DROP TABLE user;</code> would be executed. To protect against
against this we <strong>Highly</strong> recommend you create a specific MySQL user with restricted this we <strong>Highly</strong> recommend you create a specific MySQL user with restricted permissions.
permissions.
</p> </p>
</InfoBox> ),
</div>
);
}; };
export const featureInfoBox = () => { const InfoBoxTemplate: Story<InfoBoxProps> = (args) => <InfoBox {...args} />;
const { containerWidth } = getKnobs(); export const infoBox = InfoBoxTemplate.bind({});
infoBox.args = defaultProps;
const FeatureInfoBoxTemplate: Story<FeatureInfoBoxProps> = (args) => <FeatureInfoBox {...args}></FeatureInfoBox>;
export const featureInfoBox = FeatureInfoBoxTemplate.bind({});
featureInfoBox.args = defaultProps;
const DismissableTemplate: Story<DismissableFeatureInfoBoxProps> = (args) => {
const onResetClick = () => {
localStorage.removeItem(FEATUREINFOBOX_PERSISTENCE_ID_PREFIX.concat(args.persistenceId));
location.reload();
};
return ( return (
<div style={{ width: containerWidth }}> <div>
<FeatureInfoBox <div>
title="Transformations" <DismissableFeatureInfoBox {...args} />
url={'http://www.grafana.com'} </div>
featureState={FeatureState.beta} <div
onDismiss={() => { className={css`
alert('onDismiss clicked'); margin-top: 24px;
}} `}
> >
Transformations allow you to join, calculate, re-order, hide and rename your query results before being <Button onClick={onResetClick}>Reset DismissableFeatureInfoBox</Button>
visualized. <br /> </div>
Many transforms are not suitable if you&apos;re 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> </div>
); );
}; };
export const dismissableFeatureInfoBox = DismissableTemplate.bind({});
dismissableFeatureInfoBox.args = defaultProps;
...@@ -34,7 +34,7 @@ export const InfoBox = React.memo( ...@@ -34,7 +34,7 @@ export const InfoBox = React.memo(
({ title, className, children, branded, url, urlTitle, onDismiss, severity = 'info', ...otherProps }, ref) => { ({ title, className, children, branded, url, urlTitle, onDismiss, severity = 'info', ...otherProps }, ref) => {
const theme = useTheme(); const theme = useTheme();
const styles = getInfoBoxStyles(theme, severity); 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 ( return (
<div className={wrapperClassName} {...otherProps} ref={ref}> <div className={wrapperClassName} {...otherProps} ref={ref}>
......
...@@ -103,6 +103,7 @@ export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu'; ...@@ -103,6 +103,7 @@ export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
export { SeriesIcon } from './VizLegend/SeriesIcon'; export { SeriesIcon } from './VizLegend/SeriesIcon';
export { InfoBox } from './InfoBox/InfoBox'; export { InfoBox } from './InfoBox/InfoBox';
export { FeatureBadge, FeatureInfoBox } from './InfoBox/FeatureInfoBox'; export { FeatureBadge, FeatureInfoBox } from './InfoBox/FeatureInfoBox';
export { DismissableFeatureInfoBox } from './InfoBox/DismissableFeatureInfoBox';
export { JSONFormatter } from './JSONFormatter/JSONFormatter'; export { JSONFormatter } from './JSONFormatter/JSONFormatter';
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer'; export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
......
...@@ -4,11 +4,13 @@ import { ...@@ -4,11 +4,13 @@ import {
Button, Button,
Container, Container,
CustomScrollbar, CustomScrollbar,
FeatureInfoBox,
stylesFactory, stylesFactory,
Themeable,
DismissableFeatureInfoBox,
useTheme, useTheme,
ValuePicker, ValuePicker,
VerticalGroup, VerticalGroup,
withTheme,
} from '@grafana/ui'; } from '@grafana/ui';
import { import {
DataFrame, DataFrame,
...@@ -31,7 +33,7 @@ import { TransformationsEditorTransformation } from './types'; ...@@ -31,7 +33,7 @@ import { TransformationsEditorTransformation } from './types';
import { PanelNotSupported } from '../PanelEditor/PanelNotSupported'; import { PanelNotSupported } from '../PanelEditor/PanelNotSupported';
import { AppNotificationSeverity } from '../../../../types'; import { AppNotificationSeverity } from '../../../../types';
interface TransformationsEditorProps { interface TransformationsEditorProps extends Themeable {
panel: PanelModel; panel: PanelModel;
} }
...@@ -40,7 +42,7 @@ interface State { ...@@ -40,7 +42,7 @@ interface State {
transformations: TransformationsEditorTransformation[]; transformations: TransformationsEditorTransformation[];
} }
export class TransformationsEditor extends React.PureComponent<TransformationsEditorProps, State> { class UnThemedTransformationsEditor extends React.PureComponent<TransformationsEditorProps, State> {
subscription?: Unsubscribable; subscription?: Unsubscribable;
constructor(props: TransformationsEditorProps) { constructor(props: TransformationsEditorProps) {
...@@ -208,9 +210,16 @@ export class TransformationsEditor extends React.PureComponent<TransformationsEd ...@@ -208,9 +210,16 @@ export class TransformationsEditor extends React.PureComponent<TransformationsEd
renderNoAddedTransformsState() { renderNoAddedTransformsState() {
return ( return (
<VerticalGroup spacing={'lg'}> <>
<Container grow={1}> <Container grow={1}>
<FeatureInfoBox title="Transformations" url={getDocsLink(DocsId.Transformations)}> <DismissableFeatureInfoBox
title="Transformations"
className={css`
margin-bottom: ${this.props.theme.spacing.lg};
`}
persistenceId="transformationsFeaturesInfoBox"
url={getDocsLink(DocsId.Transformations)}
>
<p> <p>
Transformations allow you to join, calculate, re-order, hide and rename your query results before being Transformations allow you to join, calculate, re-order, hide and rename your query results before being
visualized. <br /> visualized. <br />
...@@ -218,7 +227,7 @@ export class TransformationsEditor extends React.PureComponent<TransformationsEd ...@@ -218,7 +227,7 @@ export class TransformationsEditor extends React.PureComponent<TransformationsEd
supports time series. <br /> supports time series. <br />
It can help to switch to Table visualization to understand what a transformation is doing. <br /> It can help to switch to Table visualization to understand what a transformation is doing. <br />
</p> </p>
</FeatureInfoBox> </DismissableFeatureInfoBox>
</Container> </Container>
<VerticalGroup> <VerticalGroup>
{standardTransformersRegistry.list().map((t) => { {standardTransformersRegistry.list().map((t) => {
...@@ -236,7 +245,7 @@ export class TransformationsEditor extends React.PureComponent<TransformationsEd ...@@ -236,7 +245,7 @@ export class TransformationsEditor extends React.PureComponent<TransformationsEd
); );
})} })}
</VerticalGroup> </VerticalGroup>
</VerticalGroup> </>
); );
} }
...@@ -299,3 +308,5 @@ const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -299,3 +308,5 @@ const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => {
`, `,
}; };
}); });
export const TransformationsEditor = withTheme(UnThemedTransformationsEditor);
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