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
This commit is contained in:
Oscar Kilhed
2021-02-02 15:16:31 +01:00
committed by GitHub
parent 7a4c32d703
commit 99acad4448
6 changed files with 121 additions and 67 deletions

View File

@ -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<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';

View File

@ -1,19 +1,18 @@
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(
) : ( ) : (
<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';

View File

@ -1,8 +1,17 @@
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 {
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'); children: (
const url = text('Url', 'http://docs.grafana.org/features/datasources/mysql/'); <p>
const severity = select('Severity', ['success', 'warning', 'error', 'info'], 'info'); 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,
return { containerWidth, severity, title, url }; statements like <code>USE otherdb;</code> and <code>DROP TABLE user;</code> would be executed. To protect against
this we <strong>Highly</strong> recommend you create a specific MySQL user with restricted permissions.
</p>
),
}; };
export const basic = () => { const InfoBoxTemplate: Story<InfoBoxProps> = (args) => <InfoBox {...args} />;
const { containerWidth, severity, title, url } = 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>
<InfoBox <div>
title={title} <DismissableFeatureInfoBox {...args} />
url={url} </div>
severity={severity} <div
onDismiss={() => { className={css`
alert('onDismiss clicked'); margin-top: 24px;
}} `}
> >
<p> <Button onClick={onResetClick}>Reset DismissableFeatureInfoBox</Button>
The database user should only be granted SELECT permissions on the specified database &amp; tables you want to </div>
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
against this we <strong>Highly</strong> recommend you create a specific MySQL user with restricted
permissions.
</p>
</InfoBox>
</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 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;

View File

@ -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}>

View File

@ -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';

View File

@ -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';
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 {
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
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
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
); );
})} })}
</VerticalGroup> </VerticalGroup>
</VerticalGroup> </>
); );
} }
@ -299,3 +308,5 @@ const getTransformationCardStyles = stylesFactory((theme: GrafanaTheme) => {
`, `,
}; };
}); });
export const TransformationsEditor = withTheme(UnThemedTransformationsEditor);