Skip to content

Commit

Permalink
[Dashboard][ES|QL] Allow creating a dashboard with ES|QL chart even w…
Browse files Browse the repository at this point in the history
…hen there are no dataviews (elastic#196658)

## Summary

Closes elastic#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 <[email protected]>
  • Loading branch information
stratoula and kibanamachine authored Oct 23, 2024
1 parent 7e63ee7 commit af139b4
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 10 deletions.
5 changes: 3 additions & 2 deletions src/plugins/dashboard/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"urlForwarding",
"presentationUtil",
"visualizations",
"unifiedSearch"
"unifiedSearch",
],
"optionalPlugins": [
"home",
Expand All @@ -35,7 +35,8 @@
"taskManager",
"serverless",
"noDataPage",
"observabilityAIAssistant"
"observabilityAIAssistant",
"lens"
],
"requiredBundles": [
"kibanaReact",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}: {
Expand All @@ -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(() =>
Expand All @@ -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 }) => {
Expand All @@ -54,7 +146,7 @@ export const DashboardAppNoDataPage = ({

return (
<AnalyticsNoDataPageKibanaProvider {...analyticsServices}>
<AnalyticsNoDataPage onDataViewCreated={onDataViewCreated} />
<AnalyticsNoDataPage onDataViewCreated={onDataViewCreated} onTryESQL={onTryESQL} />
</AnalyticsNoDataPageKibanaProvider>
);
};
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/dashboard/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -96,6 +97,7 @@ export interface DashboardSetupDependencies {
urlForwarding: UrlForwardingSetup;
unifiedSearch: UnifiedSearchPublicPluginStart;
observabilityAIAssistant?: ObservabilityAIAssistantPublicSetup;
lens?: LensPublicSetup;
}

export interface DashboardStartDependencies {
Expand All @@ -120,6 +122,7 @@ export interface DashboardStartDependencies {
customBranding: CustomBrandingStart;
serverless?: ServerlessPluginStart;
noDataPage?: NoDataPagePluginStart;
lens?: LensPublicStart;
observabilityAIAssistant?: ObservabilityAIAssistantPublicStart;
}

Expand Down
3 changes: 3 additions & 0 deletions src/plugins/dashboard/public/services/kibana_services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/plugins/dashboard/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/**/*"]
}
14 changes: 10 additions & 4 deletions test/functional/apps/dashboard/group6/dashboard_esql_no_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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`);
});
});
}

0 comments on commit af139b4

Please sign in to comment.