diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.test.tsx
index 7f7af75146b..b23726c0288 100644
--- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.test.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/EmptyTransformationsMessage.test.tsx
@@ -117,4 +117,102 @@ describe('EmptyTransformationsMessage', () => {
expect(screen.getByTestId(selectors.components.Transforms.addTransformationButton)).toBeInTheDocument();
});
});
+
+ describe('SQL card disabled state', () => {
+ beforeEach(() => {
+ config.featureToggles.transformationsEmptyPlaceholder = true;
+ config.featureToggles.sqlExpressions = true;
+ });
+
+ // Helper to check if the info icon button is present (rendered when disabled)
+ const getInfoIconButton = (container: HTMLElement) => {
+ // The IconButton for info renders as a button with an SVG icon
+ // When disabled, there are 2 buttons: the card button and the info icon button
+ const buttons = container.querySelectorAll('button');
+ return buttons.length > 1 ? buttons[1] : null;
+ };
+
+ it('should show disabled SQL card with info icon when isSqlApplicable is false', () => {
+ render(
+
+ );
+
+ const sqlCard = screen.getByTestId('go-to-queries-button');
+ // Should show info icon button when disabled (2 buttons total)
+ expect(getInfoIconButton(sqlCard)).toBeInTheDocument();
+ });
+
+ it('should not call onGoToQueries when SQL card is disabled and clicked', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ const sqlCard = screen.getByTestId('go-to-queries-button');
+ const button = sqlCard.querySelector('button');
+ await user.click(button!);
+
+ // onGoToQueries should NOT be called when disabled
+ expect(onGoToQueries).not.toHaveBeenCalled();
+ });
+
+ it('should call onGoToQueries when SQL card is enabled and clicked', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ const sqlCard = screen.getByTestId('go-to-queries-button');
+ const button = sqlCard.querySelector('button');
+ await user.click(button!);
+
+ expect(onGoToQueries).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not show info icon when SQL card is enabled', () => {
+ render(
+
+ );
+
+ const sqlCard = screen.getByTestId('go-to-queries-button');
+ // Should NOT show info icon button when enabled (only 1 button)
+ expect(getInfoIconButton(sqlCard)).toBeNull();
+ });
+
+ it('should default to enabled when isSqlApplicable is not provided', () => {
+ render(
+
+ );
+
+ const sqlCard = screen.getByTestId('go-to-queries-button');
+ // Should NOT show info icon button when default (enabled)
+ expect(getInfoIconButton(sqlCard)).toBeNull();
+ });
+ });
});
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx
index d6aa23f6073..307889d5439 100644
--- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.test.tsx
@@ -11,9 +11,11 @@ import {
toDataFrame,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
+import { DataSourceSrv, getDataSourceSrv } from '@grafana/runtime';
import { SceneDataTransformer, SceneQueryRunner } from '@grafana/scenes';
import config from 'app/core/config';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
+import { ExpressionDatasourceUID } from 'app/features/expressions/types';
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
import { DashboardDataDTO } from 'app/types/dashboard';
@@ -24,14 +26,40 @@ import { testDashboard } from '../testfiles/testDashboard';
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
+// Mock getDataSourceSrv
+jest.mock('@grafana/runtime', () => {
+ const actual = jest.requireActual('@grafana/runtime');
+ return {
+ ...actual,
+ getDataSourceSrv: jest.fn(() => ({
+ getInstanceSettings: jest.fn(),
+ })),
+ };
+});
+
+const getDataSourceSrvMock = getDataSourceSrv as jest.MockedFunction;
+
+// Helper to create DataSourceSrv mock with custom getInstanceSettings
+const createMockDataSourceSrv = (
+ getInstanceSettingsFn: (ref: { uid?: string; type?: string } | undefined) => unknown
+): DataSourceSrv =>
+ ({
+ get: jest.fn(),
+ getList: jest.fn(),
+ getInstanceSettings: getInstanceSettingsFn,
+ reload: jest.fn(),
+ registerRuntimeDataSource: jest.fn(),
+ }) as unknown as DataSourceSrv;
+
function createModelMock(
panelData: PanelData,
transformations?: DataTransformerConfig[],
- onChangeTransformationsMock?: Function
+ onChangeTransformationsMock?: Function,
+ queries: Array<{ refId: string; datasource?: { uid?: string; type?: string } }> = []
) {
return {
getDataTransformer: () => new SceneDataTransformer({ data: panelData, transformations: transformations || [] }),
- getQueryRunner: () => new SceneQueryRunner({ queries: [], data: panelData }),
+ getQueryRunner: () => new SceneQueryRunner({ queries, data: panelData }),
onChangeTransformations: onChangeTransformationsMock,
} as unknown as PanelDataTransformationsTab;
}
@@ -208,6 +236,149 @@ describe('PanelDataTransformationsTab', () => {
});
});
+describe('SQL Expression applicability', () => {
+ standardTransformersRegistry.setInit(getStandardTransformers);
+
+ let originalTransformationsToggle: boolean | undefined;
+ let originalSqlToggle: boolean | undefined;
+
+ beforeEach(() => {
+ originalTransformationsToggle = config.featureToggles.transformationsEmptyPlaceholder;
+ originalSqlToggle = config.featureToggles.sqlExpressions;
+ config.featureToggles.transformationsEmptyPlaceholder = true;
+ config.featureToggles.sqlExpressions = true;
+ });
+
+ afterEach(() => {
+ config.featureToggles.transformationsEmptyPlaceholder = originalTransformationsToggle;
+ config.featureToggles.sqlExpressions = originalSqlToggle;
+ jest.clearAllMocks();
+ });
+
+ // Helper to check if the info icon button is present (rendered when disabled)
+ // The IconButton for info renders as a button with an SVG icon
+ // When disabled, there are 2 buttons: the card button and the info icon button
+ const getInfoIconButton = (container: HTMLElement) => {
+ const buttons = container.querySelectorAll('button');
+ return buttons.length > 1 ? buttons[1] : null;
+ };
+
+ it('should enable SQL card when datasource is a backend datasource', async () => {
+ getDataSourceSrvMock.mockReturnValue(
+ createMockDataSourceSrv(() => ({
+ uid: 'prometheus',
+ name: 'Prometheus',
+ meta: { backend: true },
+ }))
+ );
+
+ const modelMock = createModelMock(mockData, [], undefined, [
+ { refId: 'A', datasource: { uid: 'prometheus', type: 'prometheus' } },
+ ]);
+
+ render();
+
+ const sqlCard = await screen.findByTestId('go-to-queries-button');
+ // Card should NOT have disabled background styling - check it's clickable
+ expect(sqlCard).toBeInTheDocument();
+ // The card should not show the disabled info icon button
+ expect(getInfoIconButton(sqlCard)).toBeNull();
+ });
+
+ it('should disable SQL card when datasource is frontend-only', async () => {
+ getDataSourceSrvMock.mockReturnValue(
+ createMockDataSourceSrv(() => ({
+ uid: 'googlesheets',
+ name: 'Google Sheets',
+ meta: { backend: false, isBackend: false },
+ }))
+ );
+
+ const modelMock = createModelMock(mockData, [], undefined, [
+ { refId: 'A', datasource: { uid: 'googlesheets', type: 'grafana-googlesheets-datasource' } },
+ ]);
+
+ render();
+
+ const sqlCard = await screen.findByTestId('go-to-queries-button');
+ // Card should show the disabled info icon button
+ expect(getInfoIconButton(sqlCard)).toBeInTheDocument();
+ });
+
+ it('should enable SQL card when datasource settings cannot be found', async () => {
+ // Return undefined for getInstanceSettings - simulating unknown datasource
+ getDataSourceSrvMock.mockReturnValue(createMockDataSourceSrv(() => undefined));
+
+ const modelMock = createModelMock(mockData, [], undefined, [
+ { refId: 'A', datasource: { uid: 'unknown-ds', type: 'unknown' } },
+ ]);
+
+ render();
+
+ const sqlCard = await screen.findByTestId('go-to-queries-button');
+ // Card should NOT be disabled when we can't determine datasource type
+ expect(getInfoIconButton(sqlCard)).toBeNull();
+ });
+
+ it('should skip expression queries when checking SQL applicability', async () => {
+ getDataSourceSrvMock.mockReturnValue(
+ createMockDataSourceSrv((ref) => {
+ if (ref?.uid === ExpressionDatasourceUID) {
+ // Expression datasource - this should be skipped
+ return { uid: ExpressionDatasourceUID, name: 'Expression', meta: { backend: false } };
+ }
+ // Backend datasource
+ return { uid: 'prometheus', name: 'Prometheus', meta: { backend: true } };
+ })
+ );
+
+ const modelMock = createModelMock(mockData, [], undefined, [
+ { refId: 'A', datasource: { uid: 'prometheus', type: 'prometheus' } },
+ { refId: 'B', datasource: { uid: ExpressionDatasourceUID, type: '__expr__' } }, // Expression query
+ ]);
+
+ render();
+
+ const sqlCard = await screen.findByTestId('go-to-queries-button');
+ // Should still be enabled because expression queries are skipped
+ expect(getInfoIconButton(sqlCard)).toBeNull();
+ });
+
+ it('should disable SQL card if any non-expression query uses frontend-only datasource', async () => {
+ getDataSourceSrvMock.mockReturnValue(
+ createMockDataSourceSrv((ref) => {
+ if (ref?.uid === 'googlesheets') {
+ return { uid: 'googlesheets', name: 'Google Sheets', meta: { backend: false, isBackend: false } };
+ }
+ return { uid: 'prometheus', name: 'Prometheus', meta: { backend: true } };
+ })
+ );
+
+ const modelMock = createModelMock(mockData, [], undefined, [
+ { refId: 'A', datasource: { uid: 'prometheus', type: 'prometheus' } },
+ { refId: 'B', datasource: { uid: 'googlesheets', type: 'grafana-googlesheets-datasource' } },
+ ]);
+
+ render();
+
+ const sqlCard = await screen.findByTestId('go-to-queries-button');
+ // Card should be disabled because one datasource is frontend-only
+ expect(getInfoIconButton(sqlCard)).toBeInTheDocument();
+ });
+
+ it('should enable SQL card when there are no queries', async () => {
+ getDataSourceSrvMock.mockReturnValue(createMockDataSourceSrv(() => undefined));
+
+ const modelMock = createModelMock(mockData, [], undefined, []);
+
+ render();
+
+ const sqlCard = await screen.findByTestId('go-to-queries-button');
+ // Card should be enabled when there are no queries
+ expect(getInfoIconButton(sqlCard)).toBeNull();
+ });
+});
+
function setupTabScene(panelId: string) {
const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
const panel = findVizPanelByKey(scene, panelId)!;