Transformations: Add applicability indicators to empty state

This commit is contained in:
Develer
2025-11-25 13:20:05 +01:00
parent a2f14d7306
commit 157fc6315b
4 changed files with 77 additions and 5 deletions
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { DataTransformerID, standardTransformersRegistry, TransformerRegistryItem } from '@grafana/data';
import { DataFrame, DataTransformerID, standardTransformersRegistry, TransformerRegistryItem } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
@@ -16,6 +16,8 @@ interface EmptyTransformationsProps {
onShowPicker: () => void;
onGoToQueries?: () => void;
onAddTransformation?: (transformationId: string) => void;
data?: DataFrame[];
isSqlApplicable?: boolean;
}
const TRANSFORMATION_IDS = [
@@ -60,6 +62,7 @@ export function LegacyEmptyTransformationsMessage({ onShowPicker }: { onShowPick
export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps) {
const hasGoToQueries = props.onGoToQueries != null;
const hasAddTransformation = props.onAddTransformation != null;
const isSqlApplicable = props.isSqlApplicable !== false; // Default to true if not provided
// Get transformations from registry
const transformations = useMemo(() => {
@@ -69,6 +72,9 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps)
}, []);
const handleSqlTransformationClick = () => {
if (!isSqlApplicable) {
return;
}
reportInteraction('dashboards_expression_interaction', {
action: 'add_expression',
expression_type: 'sql',
@@ -110,6 +116,11 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps)
imageUrl={config.theme2.isDark ? sqlDarkImage : sqlLightImage}
onClick={handleSqlTransformationClick}
testId="go-to-queries-button"
isDisabled={!isSqlApplicable}
disabledTooltip={t(
'dashboard-scene.empty-transformations-message.sql-not-applicable',
'SQL expressions require backend data sources'
)}
/>
)}
{hasAddTransformation &&
@@ -121,6 +132,7 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps)
showIllustrations={true}
showPluginState={false}
showTags={false}
data={props.data}
/>
))}
</Grid>
@@ -5,6 +5,7 @@ import { useCallback, useMemo, useState } from 'react';
import { DataTransformerConfig, GrafanaTheme2, PanelData } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import {
SceneObjectBase,
SceneComponentProps,
@@ -18,6 +19,7 @@ import { Button, ButtonGroup, ConfirmModal, Tab, useStyles2 } from '@grafana/ui'
import { TransformationOperationRows } from 'app/features/dashboard/components/TransformationsEditor/TransformationOperationRows';
import { ExpressionQueryType } from 'app/features/expressions/types';
import { ExpressionDatasourceUID } from '../../../expressions/types';
import { getQueryRunnerFor } from '../../utils/utils';
import { EmptyTransformationsMessage } from './EmptyTransformationsMessage';
@@ -83,6 +85,35 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
: [];
}, [transformsWrongType]);
// Check if SQL expressions are applicable (all datasources must be backend datasources)
const isSqlApplicable = useMemo(() => {
const queryRunner = model.getQueryRunner();
const queries = queryRunner.state.queries;
if (!queries || queries.length === 0) {
return true; // If no queries, SQL is theoretically applicable
}
// Check each query's datasource
for (const query of queries) {
const datasourceRef = query.datasource;
// Skip expression queries
if (datasourceRef && 'uid' in datasourceRef && datasourceRef.uid === ExpressionDatasourceUID) {
continue;
}
const dsSettings = getDataSourceSrv().getInstanceSettings(datasourceRef);
// If we can't get datasource settings or it's not a backend datasource, SQL is not applicable
if (!dsSettings || (!dsSettings.meta.backend && !dsSettings.meta.isBackend)) {
return false;
}
}
return true;
}, [model]);
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
const [confirmModalOpen, setConfirmModalOpen] = useState<boolean>(false);
@@ -154,6 +185,8 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
onShowPicker={openDrawer}
onGoToQueries={onGoToQueries}
onAddTransformation={onAddTransformation}
data={sourceData.data.series}
isSqlApplicable={isSqlApplicable}
/>
{transformationsDrawer}
</>
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Card, useStyles2 } from '@grafana/ui';
import { Card, IconButton, useStyles2 } from '@grafana/ui';
export interface SqlExpressionCardProps {
name: string;
@@ -9,13 +9,24 @@ export interface SqlExpressionCardProps {
imageUrl?: string;
onClick: () => void;
testId?: string;
isDisabled?: boolean;
disabledTooltip?: string;
}
export function SqlExpressionCard({ name, description, imageUrl, onClick, testId }: SqlExpressionCardProps) {
export function SqlExpressionCard({
name,
description,
imageUrl,
onClick,
testId,
isDisabled,
disabledTooltip,
}: SqlExpressionCardProps) {
const styles = useStyles2(getSqlExpressionCardStyles);
const cardClasses = isDisabled ? cx(styles.card, styles.cardDisabled) : styles.card;
return (
<Card className={styles.card} data-testid={testId} onClick={onClick} noMargin>
<Card className={cardClasses} data-testid={testId} onClick={onClick} noMargin>
<Card.Heading className={styles.heading}>
<div className={styles.titleRow}>
<span>{name}</span>
@@ -28,6 +39,9 @@ export function SqlExpressionCard({ name, description, imageUrl, onClick, testId
<img className={styles.image} src={imageUrl} alt={name} />
</span>
)}
{isDisabled && disabledTooltip && (
<IconButton className={styles.cardApplicableInfo} name="info-circle" tooltip={disabledTooltip} />
)}
</Card.Description>
</Card>
);
@@ -67,5 +81,17 @@ function getSqlExpressionCardStyles(theme: GrafanaTheme2) {
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),
}),
};
}
+1
View File
@@ -5942,6 +5942,7 @@
"add-transformation": "Add transformation",
"show-more": "Show more",
"sql-name": "Transform with SQL",
"sql-not-applicable": "SQL expressions require backend data sources",
"sql-transformation-description": "Manipulate your data using MySQL-like syntax"
},
"general-settings-edit-view": {