From af139b4038ad758ad0896492bf7aea0418334ef4 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Wed, 23 Oct 2024 08:32:01 +0200 Subject: [PATCH] [Dashboard][ES|QL] Allow creating a dashboard with ES|QL chart even when there are no dataviews (#196658) ## Summary Closes https://github.com/elastic/kibana/issues/176159 Try ES|QL button now navigates to dashboard with an ES|QL chart embedded. ![meow](https://github.com/user-attachments/assets/47ae19f5-1ed2-49f1-aceb-1f7287f58251) ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/dashboard/kibana.jsonc | 5 +- .../no_data/dashboard_app_no_data.tsx | 100 +++++++++++++++++- src/plugins/dashboard/public/plugin.tsx | 3 + .../public/services/kibana_services.ts | 3 + src/plugins/dashboard/tsconfig.json | 1 + .../group6/dashboard_esql_no_data.ts | 14 ++- 6 files changed, 116 insertions(+), 10 deletions(-) diff --git a/src/plugins/dashboard/kibana.jsonc b/src/plugins/dashboard/kibana.jsonc index 2bf60cde55ef0..d7b0f2c16e04b 100644 --- a/src/plugins/dashboard/kibana.jsonc +++ b/src/plugins/dashboard/kibana.jsonc @@ -24,7 +24,7 @@ "urlForwarding", "presentationUtil", "visualizations", - "unifiedSearch" + "unifiedSearch", ], "optionalPlugins": [ "home", @@ -35,7 +35,8 @@ "taskManager", "serverless", "noDataPage", - "observabilityAIAssistant" + "observabilityAIAssistant", + "lens" ], "requiredBundles": [ "kibanaReact", diff --git a/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx b/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx index 15b3d00d07ec7..366726267c311 100644 --- a/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx +++ b/src/plugins/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx @@ -7,9 +7,19 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; - +import React, { useCallback, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import useAsync from 'react-use/lib/useAsync'; +import { v4 as uuidv4 } from 'uuid'; +import { + getESQLAdHocDataview, + getESQLQueryColumns, + getIndexForESQLQuery, + getInitialESQLQuery, +} from '@kbn/esql-utils'; import { withSuspense } from '@kbn/shared-ux-utility'; +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils'; import { DASHBOARD_APP_ID } from '../../dashboard_constants'; import { @@ -19,10 +29,15 @@ import { embeddableService, noDataPageService, shareService, + lensService, } from '../../services/kibana_services'; import { getDashboardBackupService } from '../../services/dashboard_backup_service'; import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; +function generateId() { + return uuidv4(); +} + export const DashboardAppNoDataPage = ({ onDataViewCreated, }: { @@ -35,7 +50,7 @@ export const DashboardAppNoDataPage = ({ noDataPage: noDataPageService, share: shareService, }; - + const [abortController, setAbortController] = useState(new AbortController()); const importPromise = import('@kbn/shared-ux-page-analytics-no-data'); const AnalyticsNoDataPageKibanaProvider = withSuspense( React.lazy(() => @@ -44,6 +59,83 @@ export const DashboardAppNoDataPage = ({ }) ) ); + + const lensHelpersAsync = useAsync(() => { + return lensService?.stateHelperApi() ?? Promise.resolve(null); + }, [lensService]); + + useEffect(() => { + return () => { + abortController?.abort(); + }; + }, [abortController]); + + const onTryESQL = useCallback(async () => { + abortController?.abort(); + if (lensHelpersAsync.value) { + const abc = new AbortController(); + const { dataViews } = dataService; + const indexName = (await getIndexForESQLQuery({ dataViews })) ?? '*'; + const dataView = await getESQLAdHocDataview(`from ${indexName}`, dataViews); + const esqlQuery = getInitialESQLQuery(dataView); + + try { + const columns = await getESQLQueryColumns({ + esqlQuery, + search: dataService.search.search, + signal: abc.signal, + timeRange: dataService.query.timefilter.timefilter.getAbsoluteTime(), + }); + + // lens suggestions api context + const context = { + dataViewSpec: dataView?.toSpec(false), + fieldName: '', + textBasedColumns: columns, + query: { esql: esqlQuery }, + }; + + setAbortController(abc); + + const chartSuggestions = lensHelpersAsync.value.suggestions(context, dataView); + if (chartSuggestions?.length) { + const [suggestion] = chartSuggestions; + + const attrs = getLensAttributesFromSuggestion({ + filters: [], + query: { + esql: esqlQuery, + }, + suggestion, + dataView, + }) as TypedLensByValueInput['attributes']; + + const lensEmbeddableInput = { + attributes: attrs, + id: generateId(), + }; + + await embeddableService.getStateTransfer().navigateToWithEmbeddablePackage('dashboards', { + state: { + type: 'lens', + input: lensEmbeddableInput, + }, + path: '#/create', + }); + } + } catch (error) { + if (error.name !== 'AbortError') { + coreServices.notifications.toasts.addWarning( + i18n.translate('dashboard.noDataviews.esqlRequestWarningMessage', { + defaultMessage: 'Unable to load columns. {errorMessage}', + values: { errorMessage: error.message }, + }) + ); + } + } + } + }, [abortController, lensHelpersAsync.value]); + const AnalyticsNoDataPage = withSuspense( React.lazy(() => importPromise.then(({ AnalyticsNoDataPage: NoDataPage }) => { @@ -54,7 +146,7 @@ export const DashboardAppNoDataPage = ({ return ( - + ); }; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index b7a920eb08ce3..a8e7cd96f38db 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -27,6 +27,7 @@ import { type CoreStart, } from '@kbn/core/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { LensPublicSetup, LensPublicStart } from '@kbn/lens-plugin/public'; import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public/plugin'; @@ -96,6 +97,7 @@ export interface DashboardSetupDependencies { urlForwarding: UrlForwardingSetup; unifiedSearch: UnifiedSearchPublicPluginStart; observabilityAIAssistant?: ObservabilityAIAssistantPublicSetup; + lens?: LensPublicSetup; } export interface DashboardStartDependencies { @@ -120,6 +122,7 @@ export interface DashboardStartDependencies { customBranding: CustomBrandingStart; serverless?: ServerlessPluginStart; noDataPage?: NoDataPagePluginStart; + lens?: LensPublicStart; observabilityAIAssistant?: ObservabilityAIAssistantPublicStart; } diff --git a/src/plugins/dashboard/public/services/kibana_services.ts b/src/plugins/dashboard/public/services/kibana_services.ts index e8b164d47b413..e3fde8c37c2a9 100644 --- a/src/plugins/dashboard/public/services/kibana_services.ts +++ b/src/plugins/dashboard/public/services/kibana_services.ts @@ -18,6 +18,7 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public/plugin' import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; +import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; @@ -40,6 +41,7 @@ export let fieldFormatService: FieldFormatsStart; export let navigationService: NavigationPublicPluginStart; export let noDataPageService: NoDataPagePluginStart | undefined; export let observabilityAssistantService: ObservabilityAIAssistantPublicStart | undefined; +export let lensService: LensPublicStart | undefined; export let presentationUtilService: PresentationUtilPluginStart; export let savedObjectsTaggingService: SavedObjectTaggingOssPluginStart | undefined; export let screenshotModeService: ScreenshotModePluginStart; @@ -63,6 +65,7 @@ export const setKibanaServices = (kibanaCore: CoreStart, deps: DashboardStartDep navigationService = deps.navigation; noDataPageService = deps.noDataPage; observabilityAssistantService = deps.observabilityAIAssistant; + lensService = deps.lens; presentationUtilService = deps.presentationUtil; savedObjectsTaggingService = deps.savedObjectsTaggingOss; serverlessService = deps.serverless; diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 57125918ef3fc..3e95675ea64c3 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -80,6 +80,7 @@ "@kbn/content-management-favorites-public", "@kbn/core-custom-branding-browser-mocks", "@kbn/core-mount-utils-browser", + "@kbn/visualization-utils", ], "exclude": ["target/**/*"] } diff --git a/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts b/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts index 148cb95a82b11..333ac7f015397 100644 --- a/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts +++ b/test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts @@ -6,14 +6,16 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - const esql = getService('esql'); - const PageObjects = getPageObjects(['discover', 'dashboard']); + const panelActions = getService('dashboardPanelActions'); + const monacoEditor = getService('monacoEditor'); + const PageObjects = getPageObjects(['dashboard']); describe('No Data Views: Try ES|QL', () => { before(async () => { @@ -26,8 +28,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('noDataViewsPrompt'); await testSubjects.click('tryESQLLink'); - await PageObjects.discover.expectOnDiscover(); - await esql.expectEsqlStatement('FROM logs* | LIMIT 10'); + await PageObjects.dashboard.expectOnDashboard('New Dashboard'); + expect(await testSubjects.exists('lnsVisualizationContainer')).to.be(true); + + await panelActions.clickInlineEdit(); + const editorValue = await monacoEditor.getCodeEditorValue(); + expect(editorValue).to.eql(`FROM logs* | LIMIT 10`); }); }); }