Transformations: State feature (alpha/beta) and more (#36630)

* Adding plugin state feature to transforms

* initial help box

* New HelpBox component

* More progress

* Testing

* Removing HelpBox, simple new design, new active state for OperationRowAction

* Updated tests

* Fixed typing issue

* Removed AlphaNotice

* Made focus and enter key trigger OnClick and sorted transformations

* Fixed e2e tests
This commit is contained in:
Torkel Ödegaard 2021-07-12 16:42:04 +02:00 committed by GitHub
parent 5e62bddd1d
commit 863b412d54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 248 additions and 319 deletions

View File

@ -15,7 +15,7 @@ e2e.scenario({
e2e.components.Tab.title('Transform').should('be.visible').click();
e2e.components.TransformTab.newTransform('Reduce').should('be.visible').click();
e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click();
e2e.components.Transforms.Reduce.calculationsLabel().should('be.visible');
},

View File

@ -1,5 +1,5 @@
import React from 'react';
import { DataFrame, DataTransformerInfo } from '../types';
import { DataFrame, DataTransformerInfo, PluginState } from '../types';
import { Registry, RegistryItem } from '../utils/Registry';
export interface TransformerUIProps<T> {
@ -19,6 +19,15 @@ export interface TransformerRegistryItem<TOptions> extends RegistryItem {
* Object describing transformer configuration
*/
transformation: DataTransformerInfo<TOptions>;
/**
* Optional feature state
*/
state?: PluginState;
/** Markdown with more detailed description and help */
help?: string;
/**
* React component used as UI for the transformer
*/

View File

@ -1,10 +0,0 @@
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
import { AlphaNotice } from './AlphaNotice';
<Meta title="MDX|AlphaNotice" component={AlphaNotice} />
# AlphaNotice
Used to indicate plugin state - Alpha, Beta or Deprecated.
<Props of={AlphaNotice}/>

View File

@ -1,20 +0,0 @@
import React from 'react';
import { PluginState } from '@grafana/data';
import { AlphaNotice } from './AlphaNotice';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import mdx from './AlphaNotice.mdx';
export default {
title: 'Overlays/AlphaNotice',
component: AlphaNotice,
decorators: [withCenteredStory, withHorizontallyCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
export const basic = () => {
return <AlphaNotice state={PluginState.alpha} text="This is an alpha feature" />;
};

View File

@ -1,37 +0,0 @@
import React, { FC, useContext } from 'react';
import { css, cx } from '@emotion/css';
import { ThemeContext } from '../../index';
import { PluginState } from '@grafana/data';
import { Icon } from '../Icon/Icon';
export interface Props {
state?: PluginState;
text?: string;
className?: string;
}
export const AlphaNotice: FC<Props> = ({ state, text, className }) => {
const tooltipContent = text || 'This feature is a work in progress and updates may include breaking changes';
const theme = useContext(ThemeContext);
const styles = cx(
className,
css`
background: ${theme.colors.primary.transparent};
color: ${theme.colors.text.secondary};
white-space: nowrap;
border-radius: 3px;
text-shadow: none;
font-size: ${theme.typography.size.sm};
padding: 0 8px;
cursor: help;
display: inline-block;
`
);
return (
<div className={styles} title={tooltipContent}>
<Icon name="exclamation-triangle" /> {state}
</div>
);
};

View File

@ -1,4 +1,4 @@
import React, { memo, cloneElement, FC, ReactNode, useCallback } from 'react';
import React, { memo, cloneElement, FC, ReactNode } from 'react';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { useTheme2, stylesFactory } from '../../themes';
@ -52,13 +52,14 @@ export const Card: CardInterface = ({ heading, description, disabled, href, onCl
const hasActions = Boolean(actions || secondaryActions);
const disableHover = disabled || (!onClick && !href);
const disableEvents = disabled && !actions;
const onCardClick = useCallback(() => (disableHover ? () => {} : onClick?.()), [disableHover, onClick]);
const onCardClick = onClick && !disabled ? onClick : undefined;
const onEnterKey = onClick && !disabled ? getEnterKeyHandler(onClick) : undefined;
return (
<CardContainer
tabIndex={disableHover ? undefined : 0}
onClick={onCardClick}
onKeyDown={onEnterKey}
disableEvents={disableEvents}
disableHover={disableHover}
href={href}
@ -87,6 +88,14 @@ export const Card: CardInterface = ({ heading, description, disabled, href, onCl
);
};
function getEnterKeyHandler(onClick: () => void) {
return (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onClick();
}
};
}
/**
* @public
*/

View File

@ -61,9 +61,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, size: IconSize, variant:
let iconColor = theme.colors.text.primary;
if (variant === 'primary') {
iconColor = theme.colors.primary.main;
iconColor = theme.colors.primary.text;
} else if (variant === 'destructive') {
iconColor = theme.colors.error.main;
iconColor = theme.colors.error.text;
}
return {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,5 +1,5 @@
export { Icon } from './Icon/Icon';
export { IconButton } from './IconButton/IconButton';
export { IconButton, IconButtonVariant } from './IconButton/IconButton';
export { ConfirmButton } from './ConfirmButton/ConfirmButton';
export { DeleteButton } from './ConfirmButton/DeleteButton';
export { Tooltip, PopoverContent } from './Tooltip/Tooltip';
@ -145,7 +145,6 @@ export {
withErrorBoundary,
} from './ErrorBoundary/ErrorBoundary';
export { ErrorWithStack } from './ErrorBoundary/ErrorWithStack';
export { AlphaNotice } from './AlphaNotice/AlphaNotice';
export { DataSourceHttpSettings } from './DataSourceSettings/DataSourceHttpSettings';
export { TLSAuthSettings } from './DataSourceSettings/TLSAuthSettings';
export { CertificationKey } from './DataSourceSettings/CertificationKey';

View File

@ -5,6 +5,7 @@ import { getElementStyles } from './elements';
import { getCardStyles } from './card';
import { getAgularPanelStyles } from './angularPanelStyles';
import { getPageStyles } from './page';
import { getMarkdownStyles } from './markdownStyles';
/** @internal */
export function GlobalStyles() {
@ -12,7 +13,13 @@ export function GlobalStyles() {
return (
<Global
styles={[getElementStyles(theme), getPageStyles(theme), getCardStyles(theme), getAgularPanelStyles(theme)]}
styles={[
getElementStyles(theme),
getPageStyles(theme),
getCardStyles(theme),
getAgularPanelStyles(theme),
getMarkdownStyles(theme),
]}
/>
);
}

View File

@ -156,6 +156,6 @@ export function getVariantStyles(variant: ThemeTypographyVariant) {
font-weight: ${variant.fontWeight};
letter-spacing: ${variant.letterSpacing};
font-family: ${variant.fontFamily};
margin-bottom: 0.35em;
margin-bottom: 0.45em;
`;
}

View File

@ -0,0 +1,8 @@
import { css } from '@emotion/react';
import { GrafanaTheme2 } from '@grafana/data';
export function getMarkdownStyles(theme: GrafanaTheme2) {
return css`
// TODO copy from _utils.scss
`;
}

View File

@ -0,0 +1,45 @@
import React from 'react';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2, renderMarkdown } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
export interface Props extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
markdown?: string;
onRemove?: () => void;
}
export const OperationRowHelp = React.memo(
React.forwardRef<HTMLDivElement, Props>(({ className, children, markdown, onRemove, ...otherProps }, ref) => {
const styles = useStyles2(getStyles);
return (
<div className={cx(styles.wrapper, className)} {...otherProps} ref={ref}>
{markdown && markdownHelper(markdown)}
{children}
</div>
);
})
);
function markdownHelper(markdown: string) {
const helpHtml = renderMarkdown(markdown);
return <div className="markdown-html" dangerouslySetInnerHTML={{ __html: helpHtml }} />;
}
OperationRowHelp.displayName = 'OperationRowHelp';
const getStyles = (theme: GrafanaTheme2) => {
const borderRadius = theme.shape.borderRadius();
return {
wrapper: css`
padding: ${theme.spacing(2)};
border: 2px solid ${theme.colors.background.secondary};
border-top: none;
border-radius: 0 0 ${borderRadius} ${borderRadius};
position: relative;
top: -4px;
`,
};
};

View File

@ -1,7 +1,7 @@
import { IconButton, IconName, stylesFactory, useTheme } from '@grafana/ui';
import { IconButton, IconName, useStyles2 } from '@grafana/ui';
import React from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
interface QueryOperationActionProps {
@ -9,18 +9,20 @@ interface QueryOperationActionProps {
title: string;
onClick: (e: React.MouseEvent) => void;
disabled?: boolean;
active?: boolean;
}
export const QueryOperationAction: React.FC<QueryOperationActionProps> = ({ icon, disabled, title, ...otherProps }) => {
const theme = useTheme();
const styles = getStyles(theme);
export const QueryOperationAction: React.FC<QueryOperationActionProps> = ({
icon,
active,
disabled,
title,
onClick,
}) => {
const styles = useStyles2(getStyles);
const onClick = (e: React.MouseEvent) => {
if (!disabled) {
otherProps.onClick(e);
}
};
return (
<div className={cx(styles.icon, active && styles.active)}>
<IconButton
name={icon}
title={title}
@ -31,15 +33,31 @@ export const QueryOperationAction: React.FC<QueryOperationActionProps> = ({ icon
type="button"
aria-label={selectors.components.QueryEditorRow.actionButton(title)}
/>
</div>
);
};
QueryOperationAction.displayName = 'QueryOperationAction';
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const getStyles = (theme: GrafanaTheme2) => {
return {
icon: css`
color: ${theme.colors.textWeak};
display: flex;
position: relative;
color: ${theme.colors.text.secondary};
`,
active: css`
&::before {
display: block;
content: ' ';
position: absolute;
left: -1px;
right: 2px;
height: 3px;
border-radius: 2px;
bottom: -8px;
background-image: ${theme.colors.gradients.brandHorizontal} !important;
}
`,
};
});
};

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { DataFrame, DataTransformerConfig, TransformerRegistryItem } from '@grafana/data';
import { HorizontalGroup } from '@grafana/ui';
@ -9,6 +9,9 @@ import {
} from 'app/core/components/QueryOperationRow/QueryOperationRow';
import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction';
import { TransformationsEditorTransformation } from './types';
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
import { useToggle } from 'react-use';
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
interface TransformationOperationRowProps {
id: string;
@ -29,20 +32,20 @@ export const TransformationOperationRow: React.FC<TransformationOperationRowProp
uiConfig,
onChange,
}) => {
const [showDebug, setShowDebug] = useState(false);
const [showDebug, toggleDebug] = useToggle(false);
const [showHelp, toggleHelp] = useToggle(false);
const renderActions = ({ isOpen }: QueryOperationRowRenderProps) => {
return (
<HorizontalGroup align="center" width="auto">
{uiConfig.state && <PluginStateInfo state={uiConfig.state} />}
<QueryOperationAction
title="Debug"
disabled={!isOpen}
icon="bug"
onClick={() => {
setShowDebug(!showDebug);
}}
title="Show/hide transform help"
icon="info-circle"
onClick={toggleHelp}
active={showHelp}
/>
<QueryOperationAction title="Debug" disabled={!isOpen} icon="bug" onClick={toggleDebug} active={showDebug} />
<QueryOperationAction title="Remove" icon="trash-alt" onClick={() => onRemove(index)} />
</HorizontalGroup>
);
@ -50,6 +53,7 @@ export const TransformationOperationRow: React.FC<TransformationOperationRowProp
return (
<QueryOperationRow id={id} index={index} title={uiConfig.name} draggable actions={renderActions}>
{showHelp && <OperationRowHelp markdown={prepMarkdown(uiConfig)} />}
<TransformationEditor
debugMode={showDebug}
index={index}
@ -61,3 +65,15 @@ export const TransformationOperationRow: React.FC<TransformationOperationRowProp
</QueryOperationRow>
);
};
function prepMarkdown(uiConfig: TransformerRegistryItem<any>) {
let helpMarkdown = uiConfig.help ?? uiConfig.description;
return `
${helpMarkdown}
<a href="https://grafana.com/docs/grafana/latest/panels/transformations/?utm_source=grafana" target="_blank" rel="noreferrer">
Read more on the documentation site
</a>
`;
}

View File

@ -10,6 +10,7 @@ import {
Input,
IconButton,
useStyles2,
Card,
} from '@grafana/ui';
import {
DataFrame,
@ -19,8 +20,8 @@ import {
PanelData,
SelectableValue,
standardTransformersRegistry,
TransformerRegistryItem,
} from '@grafana/data';
import { Card, CardProps } from '../../../../core/components/Card/Card';
import { css } from '@emotion/css';
import { selectors } from '@grafana/e2e-selectors';
import { Unsubscribable } from 'rxjs';
@ -32,6 +33,7 @@ import { TransformationsEditorTransformation } from './types';
import { PanelNotSupported } from '../PanelEditor/PanelNotSupported';
import { AppNotificationSeverity } from '../../../../types';
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
const LOCAL_STORAGE_KEY = 'dashboard.components.TransformationEditor.featureInfoBox.isDismissed';
@ -214,13 +216,15 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
renderTransformsPicker() {
const { transformations, search } = this.state;
let suffix: React.ReactNode = null;
let xforms = standardTransformersRegistry.list();
let xforms = standardTransformersRegistry.list().sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0));
if (search) {
const lower = search.toLowerCase();
const filtered = xforms.filter((t) => {
const txt = (t.name + t.description).toLowerCase();
return txt.indexOf(lower) >= 0;
});
suffix = (
<>
{filtered.length} / {xforms.length} &nbsp;&nbsp;
@ -239,6 +243,7 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
const noTransforms = !transformations?.length;
const showPicker = noTransforms || this.state.showPicker;
if (!suffix && showPicker && !noTransforms) {
suffix = (
<IconButton
@ -264,10 +269,10 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
return (
<Alert
title="Transformations"
severity="info"
onRemove={() => {
onDismiss(true);
}}
severity="info"
>
<p>
Transformations allow you to join, calculate, re-order, hide, and rename your query results before
@ -306,10 +311,7 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
return (
<TransformationCard
key={t.name}
title={t.name}
description={t.description}
actions={<Button>Select</Button>}
ariaLabel={selectors.components.TransformTab.newTransform(t.name)}
transform={t}
onClick={() => {
this.onTransformationAdd({ value: t.id });
}}
@ -363,28 +365,37 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
}
}
const TransformationCard: React.FC<CardProps> = (props) => {
interface TransformationCardProps {
transform: TransformerRegistryItem<any>;
onClick: () => void;
}
function TransformationCard({ transform, onClick }: TransformationCardProps) {
const styles = useStyles2(getStyles);
return <Card {...props} className={styles.card} />;
};
return (
<Card
className={styles.card}
heading={transform.name}
aria-label={selectors.components.TransformTab.newTransform(transform.name)}
onClick={onClick}
>
<Card.Meta>{transform.description}</Card.Meta>
{transform.state && (
<Card.Tags>
<PluginStateInfo state={transform.state} />
</Card.Tags>
)}
</Card>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
card: css`
background: ${theme.colors.background.secondary};
width: 100%;
border: none;
margin: 0;
> div {
padding: ${theme.spacing(1)};
// 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.action.hover};
box-shadow: none;
border: none;
}
`,
};

View File

@ -1,8 +1,9 @@
import React, { MouseEventHandler } from 'react';
import { GrafanaTheme2, isUnsignedPluginSignature, PanelPluginMeta, PluginState } from '@grafana/data';
import { Badge, BadgeProps, IconButton, PluginSignatureBadge, useStyles2 } from '@grafana/ui';
import { IconButton, PluginSignatureBadge, useStyles2 } from '@grafana/ui';
import { css, cx } from '@emotion/css';
import { selectors } from '@grafana/e2e-selectors';
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
interface Props {
isCurrent: boolean;
@ -142,42 +143,11 @@ interface PanelPluginBadgeProps {
}
const PanelPluginBadge: React.FC<PanelPluginBadgeProps> = ({ plugin }) => {
const display = getPanelStateBadgeDisplayModel(plugin);
if (isUnsignedPluginSignature(plugin.signature)) {
return <PluginSignatureBadge status={plugin.signature} />;
}
if (!display) {
return null;
}
return <Badge color={display.color} text={display.text} icon={display.icon} tooltip={display.tooltip} />;
return <PluginStateInfo state={plugin.state} />;
};
function getPanelStateBadgeDisplayModel(panel: PanelPluginMeta): BadgeProps | null {
switch (panel.state) {
case PluginState.deprecated:
return {
text: 'Deprecated',
color: 'red',
tooltip: `${panel.name} Panel is deprecated`,
};
case PluginState.alpha:
return {
text: 'Alpha',
color: 'blue',
tooltip: `${panel.name} Panel is experimental`,
};
case PluginState.beta:
return {
text: 'Beta',
color: 'blue',
tooltip: `${panel.name} Panel is in beta`,
};
default:
return null;
}
}
PanelPluginBadge.displayName = 'PanelPluginBadge';

View File

@ -61,9 +61,7 @@ describe('Render', () => {
render(<DataSourceSettingsPage {...mockProps} />);
expect(
screen.getByTitle('Beta Plugin: There could be bugs and minor breaking changes to this plugin')
).toBeInTheDocument();
expect(screen.getByTitle('This feature is close to complete but not fully tested')).toBeInTheDocument();
});
it('should render alpha info text if plugin state is alpha', () => {
@ -73,7 +71,7 @@ describe('Render', () => {
render(<DataSourceSettingsPage {...mockProps} />);
expect(
screen.getByTitle('Alpha Plugin: This plugin is a work in progress and updates may include breaking changes')
screen.getByTitle('This feature is experimental and future updates might not be backward compatible')
).toBeInTheDocument();
});

View File

@ -23,7 +23,7 @@ import { StoreState } from 'app/types/';
import { DataSourceSettings } from '@grafana/data';
import { Alert, Button, LinkButton } from '@grafana/ui';
import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from '../state/navModel';
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
import { selectors } from '@grafana/e2e-selectors';
import { CloudInfoBox } from './CloudInfoBox';
@ -229,7 +229,7 @@ export class DataSourceSettingsPage extends PureComponent<Props> {
<div className="gf-form">
<label className="gf-form-label width-10">Plugin state</label>
<label className="gf-form-label gf-form-label--transparent">
<PluginStateinfo state={dataSourceMeta.state} />
<PluginStateInfo state={dataSourceMeta.state} />
</label>
</div>
)}

View File

@ -1,29 +1,50 @@
import React, { FC } from 'react';
import { AlphaNotice } from '@grafana/ui';
import { Badge, BadgeProps } from '@grafana/ui';
import { PluginState } from '@grafana/data';
interface Props {
state?: PluginState;
}
function getPluginStateInfoText(state?: PluginState): string | null {
switch (state) {
case PluginState.alpha:
return 'Alpha Plugin: This plugin is a work in progress and updates may include breaking changes';
case PluginState.beta:
return 'Beta Plugin: There could be bugs and minor breaking changes to this plugin';
}
return null;
}
export const PluginStateInfo: FC<Props> = (props) => {
const display = getFeatureStateInfo(props.state);
const PluginStateinfo: FC<Props> = (props) => {
const text = getPluginStateInfoText(props.state);
if (!text) {
if (!display) {
return null;
}
return <AlphaNotice state={props.state} text={text} />;
return (
<Badge
color={display.color}
title={display.tooltip}
text={display.text}
icon={display.icon}
tooltip={display.tooltip}
/>
);
};
export default PluginStateinfo;
function getFeatureStateInfo(state?: PluginState): BadgeProps | null {
switch (state) {
case PluginState.deprecated:
return {
text: 'Deprecated',
color: 'red',
tooltip: `This feature is deprecated and will be removed in a future release`,
};
case PluginState.alpha:
return {
text: 'Alpha',
color: 'blue',
tooltip: `This feature is experimental and future updates might not be backward compatible`,
};
case PluginState.beta:
return {
text: 'Beta',
color: 'blue',
tooltip: `This feature is close to complete but not fully tested`,
};
default:
return null;
}
}

View File

@ -6,7 +6,7 @@ import { has, cloneDeep } from 'lodash';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { ErrorBoundaryAlert, HorizontalGroup, InfoBox } from '@grafana/ui';
import { ErrorBoundaryAlert, HorizontalGroup } from '@grafana/ui';
import {
DataQuery,
DataSourceApi,
@ -28,6 +28,7 @@ import { QueryOperationAction } from 'app/core/components/QueryOperationRow/Quer
import { DashboardModel } from '../../dashboard/state/DashboardModel';
import { selectors } from '@grafana/e2e-selectors';
import { PanelModel } from 'app/features/dashboard/state';
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
interface Props<TQuery extends DataQuery> {
data: PanelData;
@ -274,7 +275,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
renderActions = (props: QueryOperationRowRenderProps) => {
const { query, hideDisableQuery = false } = this.props;
const { hasTextEditMode, datasource } = this.state;
const { hasTextEditMode, datasource, showingHelp } = this.state;
const isDisabled = query.hide;
const hasEditorHelp = datasource?.components?.QueryEditorHelp;
@ -282,7 +283,12 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
return (
<HorizontalGroup width="auto">
{hasEditorHelp && (
<QueryOperationAction title="Toggle data source help" icon="question-circle" onClick={this.onToggleHelp} />
<QueryOperationAction
title="Toggle data source help"
icon="question-circle"
onClick={this.onToggleHelp}
active={showingHelp}
/>
)}
{hasTextEditMode && (
<QueryOperationAction
@ -298,6 +304,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
<QueryOperationAction
title="Disable/enable query"
icon={isDisabled ? 'eye-slash' : 'eye'}
active={isDisabled}
onClick={this.onDisableQuery}
/>
) : null}
@ -354,12 +361,12 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
<div className={rowClasses}>
<ErrorBoundaryAlert>
{showingHelp && DatasourceCheatsheet && (
<InfoBox onDismiss={this.onToggleHelp}>
<OperationRowHelp>
<DatasourceCheatsheet
onClickExample={(query) => this.onClickExample(query)}
datasource={datasource}
/>
</InfoBox>
</OperationRowHelp>
)}
{editor}
</ErrorBoundaryAlert>

View File

@ -258,7 +258,7 @@ export default class LogsCheatSheet extends PureComponent<
render() {
return (
<div>
<h2>CloudWatch Logs Cheat Sheet</h2>
<h3>CloudWatch Logs cheat sheet</h3>
{CLIQ_EXAMPLES.map((cat, i) => (
<div key={`cat-${i}`}>
<div className={`cheat-sheet-item__title ${cx(exampleCategory)}`}>{cat.category}</div>

View File

@ -214,13 +214,13 @@ a.external-link {
}
table {
margin-bottom: $line-height-base;
margin-bottom: $spacer;
}
table {
td,
th {
padding: $spacer * 0.5 $spacer;
padding: $space-xs $space-sm;
}
th {
font-weight: $font-weight-semi-bold;