diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx index 80680aa4e1c..8462fe37b60 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.tsx @@ -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} /> ))} diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx index af68e86adc4..8a9a4e0ea91 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx @@ -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(false); const [confirmModalOpen, setConfirmModalOpen] = useState(false); @@ -154,6 +185,8 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro onShowPicker={openDrawer} onGoToQueries={onGoToQueries} onAddTransformation={onAddTransformation} + data={sourceData.data.series} + isSqlApplicable={isSqlApplicable} /> {transformationsDrawer} diff --git a/public/app/features/dashboard/components/TransformationsEditor/SqlExpressionCard.tsx b/public/app/features/dashboard/components/TransformationsEditor/SqlExpressionCard.tsx index 0cb9302df2e..f1432ed3fe0 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/SqlExpressionCard.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/SqlExpressionCard.tsx @@ -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 ( - +
{name} @@ -28,6 +39,9 @@ export function SqlExpressionCard({ name, description, imageUrl, onClick, testId {name} )} + {isDisabled && disabledTooltip && ( + + )} ); @@ -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), + }), }; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index fd2b50edb93..0abb97c08f1 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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": {