Compare commits
7 Commits
sriram/SQL
...
add-transf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa0092ad74 | ||
|
|
4be0055b97 | ||
|
|
af9a0d3598 | ||
|
|
59cbd4c7d8 | ||
|
|
6bdf2dd569 | ||
|
|
3fb47efc7f | ||
|
|
157fc6315b |
@@ -117,4 +117,102 @@ describe('EmptyTransformationsMessage', () => {
|
|||||||
expect(screen.getByTestId(selectors.components.Transforms.addTransformationButton)).toBeInTheDocument();
|
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(
|
||||||
|
<EmptyTransformationsMessage
|
||||||
|
onShowPicker={onShowPicker}
|
||||||
|
onGoToQueries={onGoToQueries}
|
||||||
|
onAddTransformation={onAddTransformation}
|
||||||
|
isSqlApplicable={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<EmptyTransformationsMessage
|
||||||
|
onShowPicker={onShowPicker}
|
||||||
|
onGoToQueries={onGoToQueries}
|
||||||
|
onAddTransformation={onAddTransformation}
|
||||||
|
isSqlApplicable={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<EmptyTransformationsMessage
|
||||||
|
onShowPicker={onShowPicker}
|
||||||
|
onGoToQueries={onGoToQueries}
|
||||||
|
onAddTransformation={onAddTransformation}
|
||||||
|
isSqlApplicable={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<EmptyTransformationsMessage
|
||||||
|
onShowPicker={onShowPicker}
|
||||||
|
onGoToQueries={onGoToQueries}
|
||||||
|
onAddTransformation={onAddTransformation}
|
||||||
|
isSqlApplicable={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<EmptyTransformationsMessage
|
||||||
|
onShowPicker={onShowPicker}
|
||||||
|
onGoToQueries={onGoToQueries}
|
||||||
|
onAddTransformation={onAddTransformation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const sqlCard = screen.getByTestId('go-to-queries-button');
|
||||||
|
// Should NOT show info icon button when default (enabled)
|
||||||
|
expect(getInfoIconButton(sqlCard)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface EmptyTransformationsProps {
|
|||||||
onShowPicker: () => void;
|
onShowPicker: () => void;
|
||||||
onGoToQueries?: () => void;
|
onGoToQueries?: () => void;
|
||||||
onAddTransformation?: (transformationId: string) => void;
|
onAddTransformation?: (transformationId: string) => void;
|
||||||
|
isSqlApplicable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSFORMATION_IDS = [
|
const TRANSFORMATION_IDS = [
|
||||||
@@ -60,6 +61,7 @@ export function LegacyEmptyTransformationsMessage({ onShowPicker }: { onShowPick
|
|||||||
export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps) {
|
export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps) {
|
||||||
const hasGoToQueries = props.onGoToQueries != null;
|
const hasGoToQueries = props.onGoToQueries != null;
|
||||||
const hasAddTransformation = props.onAddTransformation != null;
|
const hasAddTransformation = props.onAddTransformation != null;
|
||||||
|
const isSqlApplicable = props.isSqlApplicable ?? true; // Default to true if not provided
|
||||||
|
|
||||||
// Get transformations from registry
|
// Get transformations from registry
|
||||||
const transformations = useMemo(() => {
|
const transformations = useMemo(() => {
|
||||||
@@ -69,6 +71,9 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps)
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSqlTransformationClick = () => {
|
const handleSqlTransformationClick = () => {
|
||||||
|
if (!isSqlApplicable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
reportInteraction('dashboards_expression_interaction', {
|
reportInteraction('dashboards_expression_interaction', {
|
||||||
action: 'add_expression',
|
action: 'add_expression',
|
||||||
expression_type: 'sql',
|
expression_type: 'sql',
|
||||||
@@ -110,6 +115,11 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps)
|
|||||||
imageUrl={config.theme2.isDark ? sqlDarkImage : sqlLightImage}
|
imageUrl={config.theme2.isDark ? sqlDarkImage : sqlLightImage}
|
||||||
onClick={handleSqlTransformationClick}
|
onClick={handleSqlTransformationClick}
|
||||||
testId="go-to-queries-button"
|
testId="go-to-queries-button"
|
||||||
|
isDisabled={!isSqlApplicable}
|
||||||
|
disabledTooltip={t(
|
||||||
|
'dashboard-scene.empty-transformations-message.sql-not-applicable',
|
||||||
|
'SQL expressions require backend data sources'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasAddTransformation &&
|
{hasAddTransformation &&
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import {
|
|||||||
toDataFrame,
|
toDataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { DataSourceSrv, getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { SceneDataTransformer, SceneQueryRunner } from '@grafana/scenes';
|
import { SceneDataTransformer, SceneQueryRunner } from '@grafana/scenes';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
|
import { ExpressionDatasourceUID } from 'app/features/expressions/types';
|
||||||
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
|
import { getStandardTransformers } from 'app/features/transformers/standardTransformers';
|
||||||
import { DashboardDataDTO } from 'app/types/dashboard';
|
import { DashboardDataDTO } from 'app/types/dashboard';
|
||||||
|
|
||||||
@@ -24,14 +26,40 @@ import { testDashboard } from '../testfiles/testDashboard';
|
|||||||
|
|
||||||
import { PanelDataTransformationsTab, PanelDataTransformationsTabRendered } from './PanelDataTransformationsTab';
|
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<typeof getDataSourceSrv>;
|
||||||
|
|
||||||
|
// 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(
|
function createModelMock(
|
||||||
panelData: PanelData,
|
panelData: PanelData,
|
||||||
transformations?: DataTransformerConfig[],
|
transformations?: DataTransformerConfig[],
|
||||||
onChangeTransformationsMock?: Function
|
onChangeTransformationsMock?: Function,
|
||||||
|
queries: Array<{ refId: string; datasource?: { uid?: string; type?: string } }> = []
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
getDataTransformer: () => new SceneDataTransformer({ data: panelData, transformations: transformations || [] }),
|
getDataTransformer: () => new SceneDataTransformer({ data: panelData, transformations: transformations || [] }),
|
||||||
getQueryRunner: () => new SceneQueryRunner({ queries: [], data: panelData }),
|
getQueryRunner: () => new SceneQueryRunner({ queries, data: panelData }),
|
||||||
onChangeTransformations: onChangeTransformationsMock,
|
onChangeTransformations: onChangeTransformationsMock,
|
||||||
} as unknown as PanelDataTransformationsTab;
|
} 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(<PanelDataTransformationsTabRendered model={modelMock} />);
|
||||||
|
|
||||||
|
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(<PanelDataTransformationsTabRendered model={modelMock} />);
|
||||||
|
|
||||||
|
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(<PanelDataTransformationsTabRendered model={modelMock} />);
|
||||||
|
|
||||||
|
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(<PanelDataTransformationsTabRendered model={modelMock} />);
|
||||||
|
|
||||||
|
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(<PanelDataTransformationsTabRendered model={modelMock} />);
|
||||||
|
|
||||||
|
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(<PanelDataTransformationsTabRendered model={modelMock} />);
|
||||||
|
|
||||||
|
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) {
|
function setupTabScene(panelId: string) {
|
||||||
const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
const scene = transformSaveModelToScene({ dashboard: testDashboard as unknown as DashboardDataDTO, meta: {} });
|
||||||
const panel = findVizPanelByKey(scene, panelId)!;
|
const panel = findVizPanelByKey(scene, panelId)!;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import { DataTransformerConfig, GrafanaTheme2, PanelData } from '@grafana/data';
|
import { DataTransformerConfig, GrafanaTheme2, PanelData } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { Trans, t } from '@grafana/i18n';
|
import { Trans, t } from '@grafana/i18n';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
SceneObjectBase,
|
SceneObjectBase,
|
||||||
SceneComponentProps,
|
SceneComponentProps,
|
||||||
@@ -18,6 +19,7 @@ import { Button, ButtonGroup, ConfirmModal, Tab, useStyles2 } from '@grafana/ui'
|
|||||||
import { TransformationOperationRows } from 'app/features/dashboard/components/TransformationsEditor/TransformationOperationRows';
|
import { TransformationOperationRows } from 'app/features/dashboard/components/TransformationsEditor/TransformationOperationRows';
|
||||||
import { ExpressionQueryType } from 'app/features/expressions/types';
|
import { ExpressionQueryType } from 'app/features/expressions/types';
|
||||||
|
|
||||||
|
import { ExpressionDatasourceUID } from '../../../expressions/types';
|
||||||
import { getQueryRunnerFor } from '../../utils/utils';
|
import { getQueryRunnerFor } from '../../utils/utils';
|
||||||
|
|
||||||
import { EmptyTransformationsMessage } from './EmptyTransformationsMessage';
|
import { EmptyTransformationsMessage } from './EmptyTransformationsMessage';
|
||||||
@@ -83,6 +85,41 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
|
|||||||
: [];
|
: [];
|
||||||
}, [transformsWrongType]);
|
}, [transformsWrongType]);
|
||||||
|
|
||||||
|
// Check if SQL expressions are applicable (all datasources must be backend datasources)
|
||||||
|
const queryRunner = model.getQueryRunner();
|
||||||
|
const { queries } = queryRunner.useState();
|
||||||
|
const isSqlApplicable = useMemo(() => {
|
||||||
|
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, default to allowing SQL expressions
|
||||||
|
// (we'd rather let the user try than incorrectly block them)
|
||||||
|
if (!dsSettings) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only disable for datasources we KNOW are frontend-only
|
||||||
|
// This is a conservative deny-list approach - we may expand this in future
|
||||||
|
if (!dsSettings.meta.backend && !dsSettings.meta.isBackend) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [queries]);
|
||||||
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
|
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
|
||||||
const [confirmModalOpen, setConfirmModalOpen] = useState<boolean>(false);
|
const [confirmModalOpen, setConfirmModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
@@ -154,6 +191,7 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
|
|||||||
onShowPicker={openDrawer}
|
onShowPicker={openDrawer}
|
||||||
onGoToQueries={onGoToQueries}
|
onGoToQueries={onGoToQueries}
|
||||||
onAddTransformation={onAddTransformation}
|
onAddTransformation={onAddTransformation}
|
||||||
|
isSqlApplicable={isSqlApplicable}
|
||||||
/>
|
/>
|
||||||
{transformationsDrawer}
|
{transformationsDrawer}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Card, useStyles2 } from '@grafana/ui';
|
import { Card, IconButton, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
export interface SqlExpressionCardProps {
|
export interface SqlExpressionCardProps {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -9,13 +9,28 @@ export interface SqlExpressionCardProps {
|
|||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
testId?: string;
|
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 styles = useStyles2(getSqlExpressionCardStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={styles.card} data-testid={testId} onClick={onClick} noMargin>
|
<Card
|
||||||
|
className={cx(styles.card, { [styles.cardDisabled]: isDisabled })}
|
||||||
|
data-testid={testId}
|
||||||
|
onClick={onClick}
|
||||||
|
noMargin
|
||||||
|
>
|
||||||
<Card.Heading className={styles.heading}>
|
<Card.Heading className={styles.heading}>
|
||||||
<div className={styles.titleRow}>
|
<div className={styles.titleRow}>
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
@@ -28,6 +43,9 @@ export function SqlExpressionCard({ name, description, imageUrl, onClick, testId
|
|||||||
<img className={styles.image} src={imageUrl} alt={name} />
|
<img className={styles.image} src={imageUrl} alt={name} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{isDisabled && disabledTooltip && (
|
||||||
|
<IconButton className={styles.cardApplicableInfo} name="info-circle" tooltip={disabledTooltip} />
|
||||||
|
)}
|
||||||
</Card.Description>
|
</Card.Description>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -67,5 +85,17 @@ function getSqlExpressionCardStyles(theme: GrafanaTheme2) {
|
|||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
marginTop: theme.spacing(2),
|
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),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5942,6 +5942,7 @@
|
|||||||
"add-transformation": "Add transformation",
|
"add-transformation": "Add transformation",
|
||||||
"show-more": "Show more",
|
"show-more": "Show more",
|
||||||
"sql-name": "Transform with SQL",
|
"sql-name": "Transform with SQL",
|
||||||
|
"sql-not-applicable": "SQL expressions require backend data sources",
|
||||||
"sql-transformation-description": "Manipulate your data using MySQL-like syntax"
|
"sql-transformation-description": "Manipulate your data using MySQL-like syntax"
|
||||||
},
|
},
|
||||||
"general-settings-edit-view": {
|
"general-settings-edit-view": {
|
||||||
|
|||||||
Reference in New Issue
Block a user