mirror of
https://github.com/grafana/grafana.git
synced 2025-09-18 22:17:57 +08:00
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:
@ -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';
|
@ -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';
|
||||||
|
@ -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/');
|
|
||||||
const severity = select('Severity', ['success', 'warning', 'error', 'info'], 'info');
|
|
||||||
|
|
||||||
return { containerWidth, severity, title, url };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const basic = () => {
|
|
||||||
const { containerWidth, severity, title, url } = getKnobs();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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 & tables you want to
|
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,
|
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'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(
|
|||||||
({ 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';
|
|||||||
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 {
|
|||||||
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);
|
||||||
|
Reference in New Issue
Block a user