mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -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.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');
|
||||
},
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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 { 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
|
||||
*/
|
||||
|
@ -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 |
@ -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';
|
||||
|
@ -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),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
`;
|
||||
}
|
||||
|
@ -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 { 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,37 +9,55 @@ 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 (
|
||||
<IconButton
|
||||
name={icon}
|
||||
title={title}
|
||||
className={styles.icon}
|
||||
disabled={!!disabled}
|
||||
onClick={onClick}
|
||||
surface="header"
|
||||
type="button"
|
||||
aria-label={selectors.components.QueryEditorRow.actionButton(title)}
|
||||
/>
|
||||
<div className={cx(styles.icon, active && styles.active)}>
|
||||
<IconButton
|
||||
name={icon}
|
||||
title={title}
|
||||
className={styles.icon}
|
||||
disabled={!!disabled}
|
||||
onClick={onClick}
|
||||
surface="header"
|
||||
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;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
@ -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}
|
||||
@ -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;
|
||||
padding: ${theme.spacing(1)};
|
||||
margin: 0;
|
||||
|
||||
// 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;
|
||||
> div {
|
||||
padding: ${theme.spacing(1)};
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user