diff --git a/jest.config.js b/jest.config.js index f9d431cf5d3..728552b3a39 100644 --- a/jest.config.js +++ b/jest.config.js @@ -73,6 +73,8 @@ module.exports = { // prevent systemjs amd extra from breaking tests. 'systemjs/dist/extras/amd': '/public/test/mocks/systemjsAMDExtra.ts', '@bsull/augurs': '/public/test/mocks/augurs.ts', + // Mock @grafana/assistant to prevent initialization errors in tests + '^@grafana/assistant$': '/public/test/mocks/assistant.ts', }, // Log the test results with dynamic Loki tags. Drone CI only reporters: ['default', ['/public/test/log-reporter.js', { enable: process.env.DRONE === 'true' }]], diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 2d546393fb4..9cf2e415b81 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1420,4 +1420,9 @@ export interface FeatureToggles { * @default false */ alertingSyncDispatchTimer?: boolean; + /** + * Enables the Query with Assistant button in the query editor + * @default false + */ + queryWithAssistant?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c5fa3f0df43..bcbc7983b8c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -2240,6 +2240,14 @@ var ( HideFromDocs: true, Expression: "false", }, + { + Name: "queryWithAssistant", + Description: "Enables the Query with Assistant button in the query editor", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaOSSBigTent, + Expression: "false", + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 05e4c263a72..ba412ff6a5f 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -46,7 +46,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly 2023-09-28,externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false 2023-10-03,enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false 2024-06-18,disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false -2023-12-06,kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false +2023-12-05,kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false 2025-06-26,kubernetesLibraryPanels,experimental,@grafana/grafana-app-platform-squad,false,true,false 2024-06-05,kubernetesDashboards,GA,@grafana/dashboards-squad,false,false,false 2025-08-01,kubernetesShortURLs,experimental,@grafana/grafana-app-platform-squad,false,true,false @@ -54,7 +54,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly 2025-08-01,kubernetesAlertingRules,experimental,@grafana/alerting-squad,false,true,false 2025-08-29,kubernetesCorrelations,experimental,@grafana/datapro,false,true,false 2025-12-09,kubernetesUnifiedStorageQuotas,experimental,@grafana/search-and-storage,false,true,false -2025-10-17,kubernetesLogsDrilldown,experimental,@grafana/observability-logs,false,true,false +2025-10-16,kubernetesLogsDrilldown,experimental,@grafana/observability-logs,false,true,false 2025-10-20,kubernetesQueryCaching,experimental,@grafana/grafana-operator-experience-squad,false,true,false 2025-04-11,dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false 2025-04-11,dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false @@ -100,8 +100,8 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly 2025-07-31,useScopeSingleNodeEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true 2025-08-29,useMultipleScopeNodesEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true 2024-11-11,logQLScope,privatePreview,@grafana/oss-big-tent,false,false,false -2024-02-28,sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,false -2025-07-24,sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true +2024-02-27,sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,false +2025-07-23,sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true 2024-02-12,kubernetesAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false 2025-05-15,kubernetesAggregatorCapTokenAuth,experimental,@grafana/grafana-app-platform-squad,false,true,false 2024-02-14,groupByVariable,experimental,@grafana/dashboards-squad,false,false,false @@ -151,11 +151,11 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly 2024-10-17,unifiedStorageBigObjectsSupport,experimental,@grafana/search-and-storage,false,false,false 2024-10-22,timeRangeProvider,experimental,@grafana/grafana-frontend-platform,false,false,false 2025-11-05,timeRangePan,experimental,@grafana/dataviz-squad,false,false,true -2025-11-21,newTimeRangeZoomShortcuts,experimental,@grafana/dataviz-squad,false,false,true +2025-11-20,newTimeRangeZoomShortcuts,experimental,@grafana/dataviz-squad,false,false,true 2024-10-24,azureMonitorDisableLogLimit,GA,@grafana/partner-datasources,false,false,false 2024-12-20,playlistsReconciler,experimental,@grafana/grafana-app-platform-squad,false,true,false 2024-11-14,passwordlessMagicLinkAuthentication,experimental,@grafana/identity-access-team,false,false,false -2024-12-19,prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,false,true +2024-12-18,prometheusSpecialCharsInLabelValues,experimental,@grafana/oss-big-tent,false,false,true 2024-11-05,enableExtensionsAdminPage,experimental,@grafana/plugins-platform-backend,false,true,false 2024-11-07,enableSCIM,preview,@grafana/identity-access-team,false,false,false 2024-11-12,crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true @@ -170,7 +170,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly 2025-07-16,alertingAIAnalyzeCentralStateHistory,experimental,@grafana/alerting-squad,false,false,false 2024-11-22,alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true 2024-12-19,unifiedStorageSearchUI,experimental,@grafana/search-and-storage,false,false,false -2024-12-13,elasticsearchCrossClusterSearch,GA,@grafana/partner-datasources,false,false,false +2024-12-12,elasticsearchCrossClusterSearch,GA,@grafana/partner-datasources,false,false,false 2024-12-13,lokiLabelNamesQueryApi,GA,@grafana/oss-big-tent,false,false,false 2024-12-27,k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false 2025-01-09,improvedExternalSessionHandlingSAML,GA,@grafana/identity-access-team,false,false,false @@ -227,7 +227,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly 2025-08-01,alertEnrichmentConditional,experimental,@grafana/alerting-squad,false,false,false 2025-06-10,alertingImportAlertmanagerAPI,experimental,@grafana/alerting-squad,false,false,false 2025-08-01,alertingImportAlertmanagerUI,experimental,@grafana/alerting-squad,false,false,false -2025-07-16,sharingDashboardImage,GA,@grafana/sharing-squad,false,false,true +2025-07-15,sharingDashboardImage,GA,@grafana/sharing-squad,false,false,true 2025-06-17,preferLibraryPanelTitle,privatePreview,@grafana/dashboards-squad,false,false,false 2025-06-24,tabularNumbers,GA,@grafana/grafana-frontend-platform,false,false,false 2025-06-25,newInfluxDSConfigPageDesign,privatePreview,@grafana/partner-datasources,false,false,false @@ -256,7 +256,7 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly 2025-10-20,newGauge,preview,@grafana/dataviz-squad,false,false,true 2025-11-12,newVizSuggestions,preview,@grafana/dataviz-squad,false,false,true 2025-12-02,externalVizSuggestions,experimental,@grafana/dataviz-squad,false,false,true -2025-12-19,heatmapRowsAxisOptions,experimental,@grafana/dataviz-squad,false,false,true +2025-12-18,heatmapRowsAxisOptions,experimental,@grafana/dataviz-squad,false,false,true 2025-10-17,preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true 2025-10-31,jaegerEnableGrpcEndpoint,experimental,@grafana/oss-big-tent,false,false,false 2025-10-17,pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false @@ -279,3 +279,4 @@ Created,Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly 2026-01-06,secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false 2026-01-07,profilesExemplars,experimental,@grafana/observability-traces-and-profiling,false,false,false 2026-01-14,alertingSyncDispatchTimer,experimental,@grafana/alerting-squad,false,true,false +2026-01-15,queryWithAssistant,experimental,@grafana/oss-big-tent,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index c2fe65ac7d8..554f7437edc 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3708,6 +3708,24 @@ "expression": "false" } }, + { + "metadata": { + "name": "queryWithAssistant", + "resourceVersion": "1768832883692", + "creationTimestamp": "2026-01-15T12:40:17Z", + "deletionTimestamp": "2026-01-19T14:25:13Z", + "annotations": { + "grafana.app/updatedTimestamp": "2026-01-19 14:28:03.692934 +0000 UTC" + } + }, + "spec": { + "description": "Enables the Query with Assistant button in the query editor", + "stage": "experimental", + "codeowner": "@grafana/oss-big-tent", + "frontend": true, + "expression": "false" + } + }, { "metadata": { "name": "recentlyViewedDashboards", diff --git a/public/app/features/query/components/QueryActionAssistantButton.test.tsx b/public/app/features/query/components/QueryActionAssistantButton.test.tsx new file mode 100644 index 00000000000..e8d7723424e --- /dev/null +++ b/public/app/features/query/components/QueryActionAssistantButton.test.tsx @@ -0,0 +1,116 @@ +import { render, screen } from '@testing-library/react'; + +import { AssistantHook, useAssistant } from '@grafana/assistant'; +import { CoreApp, DataSourceInstanceSettings } from '@grafana/data'; +import { DataQuery } from '@grafana/schema'; + +import { QueryActionAssistantButton } from './QueryActionAssistantButton'; +// Mock the assistant hook +jest.mock('@grafana/assistant', () => ({ + useAssistant: jest.fn(), + createAssistantContextItem: jest.fn(), +})); + +// Mock the runtime services that assistant depends on +const mockConfig = { + featureToggles: { + queryWithAssistant: false, + }, +}; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + usePluginLinks: jest.fn().mockReturnValue({ links: [], isLoading: false }), + get config() { + return mockConfig; + }, +})); + +const useAssistantMock = jest.mocked(useAssistant); + +const mockDataSourceInstance: DataSourceInstanceSettings = { + uid: 'test-uid', + name: 'Test Datasource', + type: 'loki', +} as DataSourceInstanceSettings; + +const mockQuery: DataQuery = { + refId: 'A', +}; + +const mockQueries: DataQuery[] = [mockQuery]; + +const defaultProps = { + query: mockQuery, + queries: mockQueries, + dataSourceInstanceSettings: mockDataSourceInstance, + app: CoreApp.Explore, + datasourceApi: null, +}; + +describe('QueryActionAssistantButton', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Default: feature toggle enabled, assistant available + mockConfig.featureToggles.queryWithAssistant = true; + useAssistantMock.mockReturnValue({ + isAvailable: true, + openAssistant: jest.fn(), + } as unknown as AssistantHook); + }); + + it('should render nothing when feature toggle is disabled', () => { + mockConfig.featureToggles.queryWithAssistant = false; + useAssistantMock.mockReturnValue({ + isAvailable: true, + openAssistant: jest.fn(), + } as unknown as AssistantHook); + + const { container } = render(); + + expect(container.firstChild).toBeNull(); + }); + + it('should render nothing when app is not Explore, Dashboard, or PanelEditor', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('should render nothing when Assistant is not available', () => { + mockConfig.featureToggles.queryWithAssistant = true; + useAssistantMock.mockReturnValue({ + isAvailable: false, + openAssistant: undefined, + } as unknown as AssistantHook); + + const { container } = render(); + + expect(container.firstChild).toBeNull(); + }); + + it('should render nothing when openAssistant is not provided', () => { + mockConfig.featureToggles.queryWithAssistant = true; + useAssistantMock.mockReturnValue({ + isAvailable: true, + openAssistant: undefined, + } as unknown as AssistantHook); + + const { container } = render(); + + expect(container.firstChild).toBeNull(); + }); + + it('should render button when feature toggle is enabled and assistant is available', () => { + mockConfig.featureToggles.queryWithAssistant = true; + const mockOpenAssistant = jest.fn(); + useAssistantMock.mockReturnValue({ + isAvailable: true, + openAssistant: mockOpenAssistant, + } as unknown as AssistantHook); + + render(); + + const button = screen.getByRole('button', { name: /query with assistant/i }); + expect(button).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/query/components/QueryActionAssistantButton.tsx b/public/app/features/query/components/QueryActionAssistantButton.tsx new file mode 100644 index 00000000000..a4f2d3d2281 --- /dev/null +++ b/public/app/features/query/components/QueryActionAssistantButton.tsx @@ -0,0 +1,154 @@ +import { useAssistant, createAssistantContextItem } from '@grafana/assistant'; +import { CoreApp, DataSourceApi, DataSourceInstanceSettings } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; +import { DataQuery, DataSourceJsonData } from '@grafana/schema'; +import { Button } from '@grafana/ui'; +import { queryIsEmpty } from 'app/core/utils/query'; + +interface QueryActionAssistantButtonProps { + query: TQuery; + queries: TQuery[]; + dataSourceInstanceSettings: DataSourceInstanceSettings; + app?: CoreApp; + datasourceApi: DataSourceApi | null; +} + +export function QueryActionAssistantButton({ + query, + queries, + dataSourceInstanceSettings, + app, + datasourceApi, +}: QueryActionAssistantButtonProps) { + const { isAvailable, openAssistant } = useAssistant(); + + // Check if the feature toggle is enabled + if (!config.featureToggles.queryWithAssistant) { + return null; + } + + if (!isAvailable || !openAssistant) { + return null; + } + + // Only show for Explore and Dashboard apps + if (app !== CoreApp.Explore && app !== CoreApp.Dashboard && app !== CoreApp.PanelEditor) { + return null; + } + + // Only show for loki and prometheus datasources + const pluginId = dataSourceInstanceSettings.type; + if (pluginId !== 'loki' && pluginId !== 'prometheus') { + return null; + } + const origin = `grafana/query-editor/${pluginId}/${app ?? CoreApp.Unknown}`; + + // Check if current query has content + const hasCurrentQuery = !queryIsEmpty(query); + const otherQueries = queries.filter((q) => q.refId !== query.refId && !queryIsEmpty(q)); + + // Build context items + const context = [ + createAssistantContextItem('datasource', { + datasourceUid: dataSourceInstanceSettings.uid, + }), + ]; + + // Add current query if it has content + if (hasCurrentQuery) { + context.push( + createAssistantContextItem('structured', { + title: t('query-operation.header.current-query', 'Current query'), + data: query, + }) + ); + } + + // Add other queries if they exist + if (otherQueries.length > 0) { + context.push( + createAssistantContextItem('structured', { + title: t('query-operation.header.other-queries', 'Other queries'), + data: { + queries: otherQueries, + }, + }) + ); + } + + // Get query display text to determine if we're creating or updating + const queryDisplayText = + hasCurrentQuery && datasourceApi?.getQueryDisplayText ? datasourceApi.getQueryDisplayText(query) : null; + + // Determine if we're creating or updating based on queryDisplayText + const isUpdating = !!queryDisplayText; + const actionText = isUpdating + ? t( + 'query-operation.header.assistant-prompt-update', + 'Help me update the current query to answer my questions and provide the insights I need.' + ) + : t( + 'query-operation.header.assistant-prompt-create', + 'Help me create a new query to answer my questions and provide the insights I need.' + ); + + // Format app name nicely + const appName = + app === CoreApp.Explore + ? t('query-operation.header.app-explore', 'Explore') + : app === CoreApp.Dashboard + ? t('query-operation.header.app-dashboard', 'Dashboard') + : ''; + + // Build the prompt with proper formatting + const codeBlockLines: string[] = []; + + if (queryDisplayText) { + codeBlockLines.push(t('query-operation.header.current-query-label', 'Current query:') + ` ${queryDisplayText}`); + } + + codeBlockLines.push( + t('query-operation.header.selected-datasource-label', 'Selected data source:') + + ` ${dataSourceInstanceSettings.name}` + ); + + if (appName) { + codeBlockLines.push(t('query-operation.header.app-label', 'App:') + ` ${appName}`); + } + + // Add actionable sentence to motivate users + const actionableSentence = isUpdating + ? t( + 'query-operation.header.assistant-actionable-update', + 'Please describe what you want to change or improve in this query.' + ) + : t( + 'query-operation.header.assistant-actionable-create', + "Please describe what you want to query and what insights you're looking for." + ); + + // Build final prompt with code block + const prompt = [actionText, '```', ...codeBlockLines, '```', actionableSentence].join('\n'); + + const handleClick = () => { + openAssistant({ + origin, + prompt, + context, + autoSend: false, + }); + }; + + return ( + + ); +} diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx index 5cc7f3467d3..4af2c5864f8 100644 --- a/public/app/features/query/components/QueryEditorRow.tsx +++ b/public/app/features/query/components/QueryEditorRow.tsx @@ -36,6 +36,7 @@ import { import { useQueryLibraryContext } from '../../explore/QueryLibrary/QueryLibraryContext'; import { ExpressionDatasourceUID } from '../../expressions/types'; +import { QueryActionAssistantButton } from './QueryActionAssistantButton'; import { QueryActionComponent, RowActionComponents } from './QueryActionComponent'; import { QueryEditorRowHeader } from './QueryEditorRowHeader'; import { QueryErrorAlert } from './QueryErrorAlert'; @@ -467,6 +468,7 @@ export class QueryEditorRow extends PureComponent { const { app, query, dataSource, onChangeDataSource, onChange, queries, renderHeaderExtras, hideRefId } = this.props; + const { datasource } = this.state; return ( extends PureComponent