Redesign Empty Transformation Panel (#115648)

Co-authored-by: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com>
This commit is contained in:
Sean Griffin
2025-12-30 16:46:29 +00:00
committed by GitHub
co-authored by Alex Spencer
parent 52698cf0da
commit 82b4ce0ece
5 changed files with 96 additions and 162 deletions
@@ -4,7 +4,7 @@ import { DataFrame, DataTransformerID, standardTransformersRegistry, Transformer
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { Box, Button, Grid, Stack, Text } from '@grafana/ui';
import { Box, Button, Stack, Text } from '@grafana/ui';
import config from 'app/core/config';
import { SqlExpressionCard } from '../../../dashboard/components/TransformationsEditor/SqlExpressionCard';
@@ -26,9 +26,6 @@ const TRANSFORMATION_IDS = [
DataTransformerID.filterByValue,
];
const GRID_COLUMNS_WITH_SQL = 5;
const GRID_COLUMNS_WITHOUT_SQL = 4;
export function LegacyEmptyTransformationsMessage({ onShowPicker }: { onShowPicker: () => void }) {
return (
<Box alignItems="center" padding={4}>
@@ -94,13 +91,25 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps)
};
const showSqlCard = hasGoToQueries && config.featureToggles.sqlExpressions;
const gridColumns = showSqlCard ? GRID_COLUMNS_WITH_SQL : GRID_COLUMNS_WITHOUT_SQL;
return (
<Box alignItems="center" padding={4}>
<Stack direction="column" alignItems="center" gap={4}>
<Box padding={2}>
<Stack direction="column" alignItems="start" gap={2}>
<Stack direction="column" alignItems="start" gap={1}>
<Text element="h3" textAlignment="start">
<Trans i18nKey="transformations.empty.add-transformation-header">Add a Transformation</Trans>
</Text>
<Text element="p" textAlignment="start" color="secondary">
<Trans i18nKey="transformations.empty.add-transformation-body">
Transformations allow data to be changed in various ways before your visualization is shown.
<br />
This includes joining data together, renaming fields, making calculations, formatting data for display,
and more.
</Trans>
</Text>
</Stack>
{(hasAddTransformation || hasGoToQueries) && (
<Grid columns={gridColumns} gap={1}>
<Stack direction="row" gap={1} wrap>
{showSqlCard && (
<SqlExpressionCard
name={t('dashboard-scene.empty-transformations-message.sql-name', 'Transform with SQL')}
@@ -125,19 +134,17 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps)
data={props.data}
/>
))}
</Grid>
</Stack>
)}
<Stack direction="row" gap={2}>
<Button
icon="plus"
variant="primary"
size="md"
onClick={handleShowMoreClick}
data-testid={selectors.components.Transforms.addTransformationButton}
>
<Trans i18nKey="dashboard-scene.empty-transformations-message.show-more">Show more</Trans>
</Button>
</Stack>
<Button
icon="plus"
variant="primary"
size="md"
onClick={handleShowMoreClick}
data-testid={selectors.components.Transforms.addTransformationButton}
>
<Trans i18nKey="dashboard-scene.empty-transformations-message.show-more">Show more</Trans>
</Button>
</Stack>
</Box>
);
@@ -1,7 +1,6 @@
import { css } from '@emotion/css';
import { Card, Text, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { Card, useStyles2 } from '@grafana/ui';
import { getCardStyles } from './getCardStyles';
export interface SqlExpressionCardProps {
name: string;
@@ -12,60 +11,15 @@ export interface SqlExpressionCardProps {
}
export function SqlExpressionCard({ name, description, imageUrl, onClick, testId }: SqlExpressionCardProps) {
const styles = useStyles2(getSqlExpressionCardStyles);
const styles = useStyles2(getCardStyles);
return (
<Card className={styles.card} data-testid={testId} onClick={onClick} noMargin>
<Card.Heading className={styles.heading}>
<div className={styles.titleRow}>
<span>{name}</span>
</div>
</Card.Heading>
<Card.Description className={styles.description}>
<span>{description}</span>
{imageUrl && (
<span>
<img className={styles.image} src={imageUrl} alt={name} />
</span>
)}
<Card className={styles.baseCard} data-testid={testId} onClick={onClick} noMargin>
<Card.Heading>{name}</Card.Heading>
<Card.Description>
<Text variant="bodySmall">{description}</Text>
{imageUrl && <img className={styles.image} src={imageUrl} alt={name} />}
</Card.Description>
</Card>
);
}
function getSqlExpressionCardStyles(theme: GrafanaTheme2) {
return {
card: css({
gridTemplateRows: 'min-content 0 1fr 0',
marginBottom: 0,
}),
heading: css({
fontWeight: 400,
'> button': {
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: theme.spacing(1),
},
}),
titleRow: css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'nowrap',
width: '100%',
}),
description: css({
fontSize: theme.typography.bodySmall.fontSize,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}),
image: css({
display: 'block',
maxWidth: '100%',
marginTop: theme.spacing(2),
}),
};
}
@@ -1,35 +1,38 @@
import { cx, css } from '@emotion/css';
import { cx } from '@emotion/css';
import {
DataFrame,
GrafanaTheme2,
TransformerRegistryItem,
TransformationApplicabilityLevels,
standardTransformersRegistry,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Badge, Card, IconButton, useStyles2, useTheme2 } from '@grafana/ui';
import { Badge, Card, IconButton, Stack, Text, useStyles2, useTheme2 } from '@grafana/ui';
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
import { getCardStyles } from './getCardStyles';
export interface TransformationCardProps {
transform: TransformerRegistryItem;
data?: DataFrame[];
fullWidth?: boolean;
onClick: (id: string) => void;
showIllustrations?: boolean;
data?: DataFrame[];
showPluginState?: boolean;
showTags?: boolean;
transform: TransformerRegistryItem;
}
export function TransformationCard({
transform,
showIllustrations,
onClick,
data = [],
fullWidth = false,
onClick,
showIllustrations,
showPluginState = true,
showTags = true,
transform,
}: TransformationCardProps) {
const theme = useTheme2();
const styles = useStyles2(getTransformationCardStyles);
const styles = useStyles2(getCardStyles, fullWidth);
// Check to see if the transform is applicable to the given data
let applicabilityScore = TransformationApplicabilityLevels.Applicable;
@@ -47,7 +50,7 @@ export function TransformationCard({
}
}
const cardClasses = !isApplicable && data.length > 0 ? cx(styles.newCard, styles.cardDisabled) : styles.newCard;
const cardClasses = cx(styles.baseCard, { [styles.cardDisabled]: !isApplicable });
const imageUrl = theme.isDark ? transform.imageDark : transform.imageLight;
const description = standardTransformersRegistry.getIfExists(transform.id)?.description;
@@ -58,15 +61,11 @@ export function TransformationCard({
onClick={() => onClick(transform.id)}
noMargin
>
<Card.Heading className={styles.heading}>
<div className={styles.titleRow}>
<span>{transform.name}</span>
{showPluginState && (
<span className={styles.pluginStateInfoWrapper}>
<PluginStateInfo state={transform.state} />
</span>
)}
</div>
<Card.Heading>
<Stack alignItems="center" justifyContent="space-between">
{transform.name}
{showPluginState && <PluginStateInfo state={transform.state} />}
</Stack>
{showTags && transform.tags && transform.tags.size > 0 && (
<div className={styles.tagsWrapper}>
{Array.from(transform.tags).map((tag) => (
@@ -75,74 +74,13 @@ export function TransformationCard({
</div>
)}
</Card.Heading>
<Card.Description className={styles.description}>
<span>{description}</span>
{showIllustrations && imageUrl && (
<span>
<img className={styles.image} src={imageUrl} alt={transform.name} />
</span>
)}
<Card.Description>
<Text variant="bodySmall">{description || ''}</Text>
{showIllustrations && imageUrl && <img className={styles.image} src={imageUrl} alt={transform.name} />}
{!isApplicable && applicabilityDescription !== null && (
<IconButton className={styles.cardApplicableInfo} name="info-circle" tooltip={applicabilityDescription} />
<IconButton className={styles.applicableInfoButton} name="info-circle" tooltip={applicabilityDescription} />
)}
</Card.Description>
</Card>
);
}
function getTransformationCardStyles(theme: GrafanaTheme2) {
return {
heading: css({
fontWeight: 400,
'> button': {
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: theme.spacing(1),
},
}),
titleRow: css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'nowrap',
width: '100%',
}),
description: css({
fontSize: theme.typography.bodySmall.fontSize,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}),
image: css({
display: 'block',
maxWidth: '100%',
marginTop: theme.spacing(2),
}),
cardDisabled: css({
backgroundColor: theme.colors.action.disabledBackground,
img: {
filter: 'grayscale(100%)',
opacity: 0.33,
},
}),
cardApplicableInfo: css({
position: 'absolute',
bottom: theme.spacing(1),
right: theme.spacing(1),
}),
newCard: css({
gridTemplateRows: 'min-content 0 1fr 0',
marginBottom: 0,
}),
pluginStateInfoWrapper: css({
marginLeft: theme.spacing(0.5),
}),
tagsWrapper: css({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(0.5),
}),
};
}
@@ -165,11 +165,12 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data
<Grid columns={3} gap={1}>
{transformations.map((transform) => (
<TransformationCard
key={transform.id}
transform={transform}
showIllustrations={showIllustrations}
onClick={onClick}
data={data}
fullWidth
key={transform.id}
onClick={onClick}
showIllustrations={showIllustrations}
transform={transform}
/>
))}
</Grid>
@@ -0,0 +1,34 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
export const getCardStyles = (theme: GrafanaTheme2, fullWidth?: boolean) => ({
baseCard: css({
maxWidth: fullWidth ? 'none' : '200px',
width: fullWidth ? '100%' : 'auto',
marginBottom: 0,
}),
image: css({
display: 'block',
maxWidth: '100%',
marginTop: theme.spacing(2),
}),
cardDisabled: css({
backgroundColor: theme.colors.action.disabledBackground,
img: {
filter: 'grayscale(100%)',
opacity: 0.33,
},
}),
applicableInfoButton: css({
position: 'absolute',
bottom: theme.spacing(1),
right: theme.spacing(1),
}),
tagsWrapper: css({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(0.5),
marginTop: theme.spacing(0.5),
}),
});