mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
5e62bddd1d
commit
863b412d54
@ -15,7 +15,7 @@ e2e.scenario({
|
|||||||
|
|
||||||
e2e.components.Tab.title('Transform').should('be.visible').click();
|
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');
|
e2e.components.Transforms.Reduce.calculationsLabel().should('be.visible');
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DataFrame, DataTransformerInfo } from '../types';
|
import { DataFrame, DataTransformerInfo, PluginState } from '../types';
|
||||||
import { Registry, RegistryItem } from '../utils/Registry';
|
import { Registry, RegistryItem } from '../utils/Registry';
|
||||||
|
|
||||||
export interface TransformerUIProps<T> {
|
export interface TransformerUIProps<T> {
|
||||||
@ -19,6 +19,15 @@ export interface TransformerRegistryItem<TOptions> extends RegistryItem {
|
|||||||
* Object describing transformer configuration
|
* Object describing transformer configuration
|
||||||
*/
|
*/
|
||||||
transformation: DataTransformerInfo<TOptions>;
|
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
|
* React component used as UI for the transformer
|
||||||
*/
|
*/
|
||||||
|
@ -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}/>
|
|
@ -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" />;
|
|
||||||
};
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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 { css, cx } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { useTheme2, stylesFactory } from '../../themes';
|
import { useTheme2, stylesFactory } from '../../themes';
|
||||||
@ -52,13 +52,14 @@ export const Card: CardInterface = ({ heading, description, disabled, href, onCl
|
|||||||
const hasActions = Boolean(actions || secondaryActions);
|
const hasActions = Boolean(actions || secondaryActions);
|
||||||
const disableHover = disabled || (!onClick && !href);
|
const disableHover = disabled || (!onClick && !href);
|
||||||
const disableEvents = disabled && !actions;
|
const disableEvents = disabled && !actions;
|
||||||
|
const onCardClick = onClick && !disabled ? onClick : undefined;
|
||||||
const onCardClick = useCallback(() => (disableHover ? () => {} : onClick?.()), [disableHover, onClick]);
|
const onEnterKey = onClick && !disabled ? getEnterKeyHandler(onClick) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer
|
<CardContainer
|
||||||
tabIndex={disableHover ? undefined : 0}
|
tabIndex={disableHover ? undefined : 0}
|
||||||
onClick={onCardClick}
|
onClick={onCardClick}
|
||||||
|
onKeyDown={onEnterKey}
|
||||||
disableEvents={disableEvents}
|
disableEvents={disableEvents}
|
||||||
disableHover={disableHover}
|
disableHover={disableHover}
|
||||||
href={href}
|
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
|
* @public
|
||||||
*/
|
*/
|
||||||
|
@ -61,9 +61,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2, size: IconSize, variant:
|
|||||||
let iconColor = theme.colors.text.primary;
|
let iconColor = theme.colors.text.primary;
|
||||||
|
|
||||||
if (variant === 'primary') {
|
if (variant === 'primary') {
|
||||||
iconColor = theme.colors.primary.main;
|
iconColor = theme.colors.primary.text;
|
||||||
} else if (variant === 'destructive') {
|
} else if (variant === 'destructive') {
|
||||||
iconColor = theme.colors.error.main;
|
iconColor = theme.colors.error.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
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 |
@ -1,5 +1,5 @@
|
|||||||
export { Icon } from './Icon/Icon';
|
export { Icon } from './Icon/Icon';
|
||||||
export { IconButton } from './IconButton/IconButton';
|
export { IconButton, IconButtonVariant } from './IconButton/IconButton';
|
||||||
export { ConfirmButton } from './ConfirmButton/ConfirmButton';
|
export { ConfirmButton } from './ConfirmButton/ConfirmButton';
|
||||||
export { DeleteButton } from './ConfirmButton/DeleteButton';
|
export { DeleteButton } from './ConfirmButton/DeleteButton';
|
||||||
export { Tooltip, PopoverContent } from './Tooltip/Tooltip';
|
export { Tooltip, PopoverContent } from './Tooltip/Tooltip';
|
||||||
@ -145,7 +145,6 @@ export {
|
|||||||
withErrorBoundary,
|
withErrorBoundary,
|
||||||
} from './ErrorBoundary/ErrorBoundary';
|
} from './ErrorBoundary/ErrorBoundary';
|
||||||
export { ErrorWithStack } from './ErrorBoundary/ErrorWithStack';
|
export { ErrorWithStack } from './ErrorBoundary/ErrorWithStack';
|
||||||
export { AlphaNotice } from './AlphaNotice/AlphaNotice';
|
|
||||||
export { DataSourceHttpSettings } from './DataSourceSettings/DataSourceHttpSettings';
|
export { DataSourceHttpSettings } from './DataSourceSettings/DataSourceHttpSettings';
|
||||||
export { TLSAuthSettings } from './DataSourceSettings/TLSAuthSettings';
|
export { TLSAuthSettings } from './DataSourceSettings/TLSAuthSettings';
|
||||||
export { CertificationKey } from './DataSourceSettings/CertificationKey';
|
export { CertificationKey } from './DataSourceSettings/CertificationKey';
|
||||||
|
@ -5,6 +5,7 @@ import { getElementStyles } from './elements';
|
|||||||
import { getCardStyles } from './card';
|
import { getCardStyles } from './card';
|
||||||
import { getAgularPanelStyles } from './angularPanelStyles';
|
import { getAgularPanelStyles } from './angularPanelStyles';
|
||||||
import { getPageStyles } from './page';
|
import { getPageStyles } from './page';
|
||||||
|
import { getMarkdownStyles } from './markdownStyles';
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export function GlobalStyles() {
|
export function GlobalStyles() {
|
||||||
@ -12,7 +13,13 @@ export function GlobalStyles() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Global
|
<Global
|
||||||
styles={[getElementStyles(theme), getPageStyles(theme), getCardStyles(theme), getAgularPanelStyles(theme)]}
|
styles={[
|
||||||
|
getElementStyles(theme),
|
||||||
|
getPageStyles(theme),
|
||||||
|
getCardStyles(theme),
|
||||||
|
getAgularPanelStyles(theme),
|
||||||
|
getMarkdownStyles(theme),
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -156,6 +156,6 @@ export function getVariantStyles(variant: ThemeTypographyVariant) {
|
|||||||
font-weight: ${variant.fontWeight};
|
font-weight: ${variant.fontWeight};
|
||||||
letter-spacing: ${variant.letterSpacing};
|
letter-spacing: ${variant.letterSpacing};
|
||||||
font-family: ${variant.fontFamily};
|
font-family: ${variant.fontFamily};
|
||||||
margin-bottom: 0.35em;
|
margin-bottom: 0.45em;
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
`;
|
||||||
|
}
|
@ -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;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { IconButton, IconName, stylesFactory, useTheme } from '@grafana/ui';
|
import { IconButton, IconName, useStyles2 } from '@grafana/ui';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
interface QueryOperationActionProps {
|
interface QueryOperationActionProps {
|
||||||
@ -9,37 +9,55 @@ interface QueryOperationActionProps {
|
|||||||
title: string;
|
title: string;
|
||||||
onClick: (e: React.MouseEvent) => void;
|
onClick: (e: React.MouseEvent) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryOperationAction: React.FC<QueryOperationActionProps> = ({ icon, disabled, title, ...otherProps }) => {
|
export const QueryOperationAction: React.FC<QueryOperationActionProps> = ({
|
||||||
const theme = useTheme();
|
icon,
|
||||||
const styles = getStyles(theme);
|
active,
|
||||||
|
disabled,
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const onClick = (e: React.MouseEvent) => {
|
|
||||||
if (!disabled) {
|
|
||||||
otherProps.onClick(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<div className={cx(styles.icon, active && styles.active)}>
|
||||||
name={icon}
|
<IconButton
|
||||||
title={title}
|
name={icon}
|
||||||
className={styles.icon}
|
title={title}
|
||||||
disabled={!!disabled}
|
className={styles.icon}
|
||||||
onClick={onClick}
|
disabled={!!disabled}
|
||||||
surface="header"
|
onClick={onClick}
|
||||||
type="button"
|
surface="header"
|
||||||
aria-label={selectors.components.QueryEditorRow.actionButton(title)}
|
type="button"
|
||||||
/>
|
aria-label={selectors.components.QueryEditorRow.actionButton(title)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
QueryOperationAction.displayName = 'QueryOperationAction';
|
QueryOperationAction.displayName = 'QueryOperationAction';
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
icon: css`
|
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;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { DataFrame, DataTransformerConfig, TransformerRegistryItem } from '@grafana/data';
|
import { DataFrame, DataTransformerConfig, TransformerRegistryItem } from '@grafana/data';
|
||||||
import { HorizontalGroup } from '@grafana/ui';
|
import { HorizontalGroup } from '@grafana/ui';
|
||||||
|
|
||||||
@ -9,6 +9,9 @@ import {
|
|||||||
} from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
} from 'app/core/components/QueryOperationRow/QueryOperationRow';
|
||||||
import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction';
|
import { QueryOperationAction } from 'app/core/components/QueryOperationRow/QueryOperationAction';
|
||||||
import { TransformationsEditorTransformation } from './types';
|
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 {
|
interface TransformationOperationRowProps {
|
||||||
id: string;
|
id: string;
|
||||||
@ -29,20 +32,20 @@ export const TransformationOperationRow: React.FC<TransformationOperationRowProp
|
|||||||
uiConfig,
|
uiConfig,
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [showDebug, setShowDebug] = useState(false);
|
const [showDebug, toggleDebug] = useToggle(false);
|
||||||
|
const [showHelp, toggleHelp] = useToggle(false);
|
||||||
|
|
||||||
const renderActions = ({ isOpen }: QueryOperationRowRenderProps) => {
|
const renderActions = ({ isOpen }: QueryOperationRowRenderProps) => {
|
||||||
return (
|
return (
|
||||||
<HorizontalGroup align="center" width="auto">
|
<HorizontalGroup align="center" width="auto">
|
||||||
|
{uiConfig.state && <PluginStateInfo state={uiConfig.state} />}
|
||||||
<QueryOperationAction
|
<QueryOperationAction
|
||||||
title="Debug"
|
title="Show/hide transform help"
|
||||||
disabled={!isOpen}
|
icon="info-circle"
|
||||||
icon="bug"
|
onClick={toggleHelp}
|
||||||
onClick={() => {
|
active={showHelp}
|
||||||
setShowDebug(!showDebug);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
<QueryOperationAction title="Debug" disabled={!isOpen} icon="bug" onClick={toggleDebug} active={showDebug} />
|
||||||
<QueryOperationAction title="Remove" icon="trash-alt" onClick={() => onRemove(index)} />
|
<QueryOperationAction title="Remove" icon="trash-alt" onClick={() => onRemove(index)} />
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
);
|
);
|
||||||
@ -50,6 +53,7 @@ export const TransformationOperationRow: React.FC<TransformationOperationRowProp
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryOperationRow id={id} index={index} title={uiConfig.name} draggable actions={renderActions}>
|
<QueryOperationRow id={id} index={index} title={uiConfig.name} draggable actions={renderActions}>
|
||||||
|
{showHelp && <OperationRowHelp markdown={prepMarkdown(uiConfig)} />}
|
||||||
<TransformationEditor
|
<TransformationEditor
|
||||||
debugMode={showDebug}
|
debugMode={showDebug}
|
||||||
index={index}
|
index={index}
|
||||||
@ -61,3 +65,15 @@ export const TransformationOperationRow: React.FC<TransformationOperationRowProp
|
|||||||
</QueryOperationRow>
|
</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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
IconButton,
|
IconButton,
|
||||||
useStyles2,
|
useStyles2,
|
||||||
|
Card,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
@ -19,8 +20,8 @@ import {
|
|||||||
PanelData,
|
PanelData,
|
||||||
SelectableValue,
|
SelectableValue,
|
||||||
standardTransformersRegistry,
|
standardTransformersRegistry,
|
||||||
|
TransformerRegistryItem,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { Card, CardProps } from '../../../../core/components/Card/Card';
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
@ -32,6 +33,7 @@ import { TransformationsEditorTransformation } from './types';
|
|||||||
import { PanelNotSupported } from '../PanelEditor/PanelNotSupported';
|
import { PanelNotSupported } from '../PanelEditor/PanelNotSupported';
|
||||||
import { AppNotificationSeverity } from '../../../../types';
|
import { AppNotificationSeverity } from '../../../../types';
|
||||||
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
|
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
|
||||||
|
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = 'dashboard.components.TransformationEditor.featureInfoBox.isDismissed';
|
const LOCAL_STORAGE_KEY = 'dashboard.components.TransformationEditor.featureInfoBox.isDismissed';
|
||||||
|
|
||||||
@ -214,13 +216,15 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
|||||||
renderTransformsPicker() {
|
renderTransformsPicker() {
|
||||||
const { transformations, search } = this.state;
|
const { transformations, search } = this.state;
|
||||||
let suffix: React.ReactNode = null;
|
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) {
|
if (search) {
|
||||||
const lower = search.toLowerCase();
|
const lower = search.toLowerCase();
|
||||||
const filtered = xforms.filter((t) => {
|
const filtered = xforms.filter((t) => {
|
||||||
const txt = (t.name + t.description).toLowerCase();
|
const txt = (t.name + t.description).toLowerCase();
|
||||||
return txt.indexOf(lower) >= 0;
|
return txt.indexOf(lower) >= 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
suffix = (
|
suffix = (
|
||||||
<>
|
<>
|
||||||
{filtered.length} / {xforms.length}
|
{filtered.length} / {xforms.length}
|
||||||
@ -239,6 +243,7 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
|||||||
|
|
||||||
const noTransforms = !transformations?.length;
|
const noTransforms = !transformations?.length;
|
||||||
const showPicker = noTransforms || this.state.showPicker;
|
const showPicker = noTransforms || this.state.showPicker;
|
||||||
|
|
||||||
if (!suffix && showPicker && !noTransforms) {
|
if (!suffix && showPicker && !noTransforms) {
|
||||||
suffix = (
|
suffix = (
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -264,10 +269,10 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
|||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
title="Transformations"
|
title="Transformations"
|
||||||
|
severity="info"
|
||||||
onRemove={() => {
|
onRemove={() => {
|
||||||
onDismiss(true);
|
onDismiss(true);
|
||||||
}}
|
}}
|
||||||
severity="info"
|
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Transformations allow you to join, calculate, re-order, hide, and rename your query results before
|
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 (
|
return (
|
||||||
<TransformationCard
|
<TransformationCard
|
||||||
key={t.name}
|
key={t.name}
|
||||||
title={t.name}
|
transform={t}
|
||||||
description={t.description}
|
|
||||||
actions={<Button>Select</Button>}
|
|
||||||
ariaLabel={selectors.components.TransformTab.newTransform(t.name)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.onTransformationAdd({ value: t.id });
|
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);
|
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) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
card: css`
|
card: css`
|
||||||
background: ${theme.colors.background.secondary};
|
margin: 0;
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
padding: ${theme.spacing(1)};
|
|
||||||
|
|
||||||
// hack because these cards use classes from a very different card for some reason
|
> div {
|
||||||
.add-data-source-item-text {
|
padding: ${theme.spacing(1)};
|
||||||
font-size: ${theme.typography.size.md};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: ${theme.colors.action.hover};
|
|
||||||
box-shadow: none;
|
|
||||||
border: none;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { MouseEventHandler } from 'react';
|
import React, { MouseEventHandler } from 'react';
|
||||||
import { GrafanaTheme2, isUnsignedPluginSignature, PanelPluginMeta, PluginState } from '@grafana/data';
|
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 { css, cx } from '@emotion/css';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { PluginStateInfo } from 'app/features/plugins/PluginStateInfo';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
@ -142,42 +143,11 @@ interface PanelPluginBadgeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PanelPluginBadge: React.FC<PanelPluginBadgeProps> = ({ plugin }) => {
|
const PanelPluginBadge: React.FC<PanelPluginBadgeProps> = ({ plugin }) => {
|
||||||
const display = getPanelStateBadgeDisplayModel(plugin);
|
|
||||||
|
|
||||||
if (isUnsignedPluginSignature(plugin.signature)) {
|
if (isUnsignedPluginSignature(plugin.signature)) {
|
||||||
return <PluginSignatureBadge status={plugin.signature} />;
|
return <PluginSignatureBadge status={plugin.signature} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!display) {
|
return <PluginStateInfo state={plugin.state} />;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Badge color={display.color} text={display.text} icon={display.icon} tooltip={display.tooltip} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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';
|
PanelPluginBadge.displayName = 'PanelPluginBadge';
|
||||||
|
@ -61,9 +61,7 @@ describe('Render', () => {
|
|||||||
|
|
||||||
render(<DataSourceSettingsPage {...mockProps} />);
|
render(<DataSourceSettingsPage {...mockProps} />);
|
||||||
|
|
||||||
expect(
|
expect(screen.getByTitle('This feature is close to complete but not fully tested')).toBeInTheDocument();
|
||||||
screen.getByTitle('Beta Plugin: There could be bugs and minor breaking changes to this plugin')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render alpha info text if plugin state is alpha', () => {
|
it('should render alpha info text if plugin state is alpha', () => {
|
||||||
@ -73,7 +71,7 @@ describe('Render', () => {
|
|||||||
render(<DataSourceSettingsPage {...mockProps} />);
|
render(<DataSourceSettingsPage {...mockProps} />);
|
||||||
|
|
||||||
expect(
|
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();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ import { StoreState } from 'app/types/';
|
|||||||
import { DataSourceSettings } from '@grafana/data';
|
import { DataSourceSettings } from '@grafana/data';
|
||||||
import { Alert, Button, LinkButton } from '@grafana/ui';
|
import { Alert, Button, LinkButton } from '@grafana/ui';
|
||||||
import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from '../state/navModel';
|
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 { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { CloudInfoBox } from './CloudInfoBox';
|
import { CloudInfoBox } from './CloudInfoBox';
|
||||||
@ -229,7 +229,7 @@ export class DataSourceSettingsPage extends PureComponent<Props> {
|
|||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<label className="gf-form-label width-10">Plugin state</label>
|
<label className="gf-form-label width-10">Plugin state</label>
|
||||||
<label className="gf-form-label gf-form-label--transparent">
|
<label className="gf-form-label gf-form-label--transparent">
|
||||||
<PluginStateinfo state={dataSourceMeta.state} />
|
<PluginStateInfo state={dataSourceMeta.state} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,29 +1,50 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { AlphaNotice } from '@grafana/ui';
|
import { Badge, BadgeProps } from '@grafana/ui';
|
||||||
import { PluginState } from '@grafana/data';
|
import { PluginState } from '@grafana/data';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
state?: PluginState;
|
state?: PluginState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginStateInfoText(state?: PluginState): string | null {
|
export const PluginStateInfo: FC<Props> = (props) => {
|
||||||
switch (state) {
|
const display = getFeatureStateInfo(props.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PluginStateinfo: FC<Props> = (props) => {
|
if (!display) {
|
||||||
const text = getPluginStateInfoText(props.state);
|
|
||||||
|
|
||||||
if (!text) {
|
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { has, cloneDeep } from 'lodash';
|
|||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
|
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
|
||||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { ErrorBoundaryAlert, HorizontalGroup, InfoBox } from '@grafana/ui';
|
import { ErrorBoundaryAlert, HorizontalGroup } from '@grafana/ui';
|
||||||
import {
|
import {
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
@ -28,6 +28,7 @@ import { QueryOperationAction } from 'app/core/components/QueryOperationRow/Quer
|
|||||||
import { DashboardModel } from '../../dashboard/state/DashboardModel';
|
import { DashboardModel } from '../../dashboard/state/DashboardModel';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
|
import { OperationRowHelp } from 'app/core/components/QueryOperationRow/OperationRowHelp';
|
||||||
|
|
||||||
interface Props<TQuery extends DataQuery> {
|
interface Props<TQuery extends DataQuery> {
|
||||||
data: PanelData;
|
data: PanelData;
|
||||||
@ -274,7 +275,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
|||||||
|
|
||||||
renderActions = (props: QueryOperationRowRenderProps) => {
|
renderActions = (props: QueryOperationRowRenderProps) => {
|
||||||
const { query, hideDisableQuery = false } = this.props;
|
const { query, hideDisableQuery = false } = this.props;
|
||||||
const { hasTextEditMode, datasource } = this.state;
|
const { hasTextEditMode, datasource, showingHelp } = this.state;
|
||||||
const isDisabled = query.hide;
|
const isDisabled = query.hide;
|
||||||
|
|
||||||
const hasEditorHelp = datasource?.components?.QueryEditorHelp;
|
const hasEditorHelp = datasource?.components?.QueryEditorHelp;
|
||||||
@ -282,7 +283,12 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
|||||||
return (
|
return (
|
||||||
<HorizontalGroup width="auto">
|
<HorizontalGroup width="auto">
|
||||||
{hasEditorHelp && (
|
{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 && (
|
{hasTextEditMode && (
|
||||||
<QueryOperationAction
|
<QueryOperationAction
|
||||||
@ -298,6 +304,7 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
|||||||
<QueryOperationAction
|
<QueryOperationAction
|
||||||
title="Disable/enable query"
|
title="Disable/enable query"
|
||||||
icon={isDisabled ? 'eye-slash' : 'eye'}
|
icon={isDisabled ? 'eye-slash' : 'eye'}
|
||||||
|
active={isDisabled}
|
||||||
onClick={this.onDisableQuery}
|
onClick={this.onDisableQuery}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@ -354,12 +361,12 @@ export class QueryEditorRow<TQuery extends DataQuery> extends PureComponent<Prop
|
|||||||
<div className={rowClasses}>
|
<div className={rowClasses}>
|
||||||
<ErrorBoundaryAlert>
|
<ErrorBoundaryAlert>
|
||||||
{showingHelp && DatasourceCheatsheet && (
|
{showingHelp && DatasourceCheatsheet && (
|
||||||
<InfoBox onDismiss={this.onToggleHelp}>
|
<OperationRowHelp>
|
||||||
<DatasourceCheatsheet
|
<DatasourceCheatsheet
|
||||||
onClickExample={(query) => this.onClickExample(query)}
|
onClickExample={(query) => this.onClickExample(query)}
|
||||||
datasource={datasource}
|
datasource={datasource}
|
||||||
/>
|
/>
|
||||||
</InfoBox>
|
</OperationRowHelp>
|
||||||
)}
|
)}
|
||||||
{editor}
|
{editor}
|
||||||
</ErrorBoundaryAlert>
|
</ErrorBoundaryAlert>
|
||||||
|
@ -258,7 +258,7 @@ export default class LogsCheatSheet extends PureComponent<
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>CloudWatch Logs Cheat Sheet</h2>
|
<h3>CloudWatch Logs cheat sheet</h3>
|
||||||
{CLIQ_EXAMPLES.map((cat, i) => (
|
{CLIQ_EXAMPLES.map((cat, i) => (
|
||||||
<div key={`cat-${i}`}>
|
<div key={`cat-${i}`}>
|
||||||
<div className={`cheat-sheet-item__title ${cx(exampleCategory)}`}>{cat.category}</div>
|
<div className={`cheat-sheet-item__title ${cx(exampleCategory)}`}>{cat.category}</div>
|
||||||
|
@ -214,13 +214,13 @@ a.external-link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
margin-bottom: $line-height-base;
|
margin-bottom: $spacer;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
padding: $spacer * 0.5 $spacer;
|
padding: $space-xs $space-sm;
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
font-weight: $font-weight-semi-bold;
|
font-weight: $font-weight-semi-bold;
|
||||||
|
Loading…
Reference in New Issue
Block a user