+
>
diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/_dashboard_viewport.scss b/src/plugins/dashboard/public/application/embeddable/viewport/_dashboard_viewport.scss
index f9fc2c0a216331..5fc17b73bcd953 100644
--- a/src/plugins/dashboard/public/application/embeddable/viewport/_dashboard_viewport.scss
+++ b/src/plugins/dashboard/public/application/embeddable/viewport/_dashboard_viewport.scss
@@ -14,3 +14,7 @@
.dshDashboardEmptyScreen {
margin-top: $euiSizeS;
}
+
+.dashboardViewport--screenshotMode .controlsWrapper--empty {
+ display:none
+}
diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx
index 82c5bab836a853..7ce3a139f773ac 100644
--- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx
+++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.tsx
@@ -117,7 +117,15 @@ export class DashboardViewport extends React.Component
) : null}
-
+
+
0
+ ? 'dshDashboardViewport-controls'
+ : ''
+ }
+ ref={this.controlsRoot}
+ />
>
) : null}
true,
+ isCompatible: async (context: ValueClickContext) => {
+ const filters = await createFiltersFromValueClickAction(context.data);
+ return filters.length > 0;
+ },
execute: async (context: ValueClickActionContext) => {
try {
const filters: Filter[] = await createFiltersFromValueClickAction(context.data);
diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts
index 098e895324aa75..b263c7d77fe3be 100644
--- a/src/plugins/data_views/common/data_views/data_views.ts
+++ b/src/plugins/data_views/common/data_views/data_views.ts
@@ -113,7 +113,17 @@ export class DataViewsService {
private savedObjectsCache?: Array
> | null;
private apiClient: IDataViewsApiClient;
private fieldFormats: FieldFormatsStartCommon;
+ /**
+ * Handler for service notifications
+ * @param toastInputFields notification content in toast format
+ * @param key used to indicate uniqueness of the notification
+ */
private onNotification: OnNotification;
+ /*
+ * Handler for service errors
+ * @param error notification content in toast format
+ * @param key used to indicate uniqueness of the error
+ */
private onError: OnError;
private dataViewCache: ReturnType;
public getCanSave: () => Promise;
@@ -333,15 +343,22 @@ export class DataViewsService {
indexPattern.fields.replaceAll(fieldsWithSavedAttrs);
} catch (err) {
if (err instanceof DataViewMissingIndices) {
- this.onNotification({ title: err.message, color: 'danger', iconType: 'alert' });
+ this.onNotification(
+ { title: err.message, color: 'danger', iconType: 'alert' },
+ `refreshFields:${indexPattern.title}`
+ );
}
- this.onError(err, {
- title: i18n.translate('dataViews.fetchFieldErrorTitle', {
- defaultMessage: 'Error fetching fields for data view {title} (ID: {id})',
- values: { id: indexPattern.id, title: indexPattern.title },
- }),
- });
+ this.onError(
+ err,
+ {
+ title: i18n.translate('dataViews.fetchFieldErrorTitle', {
+ defaultMessage: 'Error fetching fields for data view {title} (ID: {id})',
+ values: { id: indexPattern.id, title: indexPattern.title },
+ }),
+ },
+ indexPattern.title
+ );
}
};
@@ -378,16 +395,23 @@ export class DataViewsService {
return this.fieldArrayToMap(updatedFieldList, fieldAttrs);
} catch (err) {
if (err instanceof DataViewMissingIndices) {
- this.onNotification({ title: err.message, color: 'danger', iconType: 'alert' });
+ this.onNotification(
+ { title: err.message, color: 'danger', iconType: 'alert' },
+ `refreshFieldSpecMap:${title}`
+ );
return {};
}
- this.onError(err, {
- title: i18n.translate('dataViews.fetchFieldErrorTitle', {
- defaultMessage: 'Error fetching fields for data view {title} (ID: {id})',
- values: { id, title },
- }),
- });
+ this.onError(
+ err,
+ {
+ title: i18n.translate('dataViews.fetchFieldErrorTitle', {
+ defaultMessage: 'Error fetching fields for data view {title} (ID: {id})',
+ values: { id, title },
+ }),
+ },
+ title
+ );
throw err;
}
};
@@ -530,18 +554,25 @@ export class DataViewsService {
}
} catch (err) {
if (err instanceof DataViewMissingIndices) {
- this.onNotification({
- title: err.message,
- color: 'danger',
- iconType: 'alert',
- });
+ this.onNotification(
+ {
+ title: err.message,
+ color: 'danger',
+ iconType: 'alert',
+ },
+ `initFromSavedObject:${title}`
+ );
} else {
- this.onError(err, {
- title: i18n.translate('dataViews.fetchFieldErrorTitle', {
- defaultMessage: 'Error fetching fields for data view {title} (ID: {id})',
- values: { id: savedObject.id, title },
- }),
- });
+ this.onError(
+ err,
+ {
+ title: i18n.translate('dataViews.fetchFieldErrorTitle', {
+ defaultMessage: 'Error fetching fields for data view {title} (ID: {id})',
+ values: { id: savedObject.id, title },
+ }),
+ },
+ title || ''
+ );
}
}
@@ -718,7 +749,10 @@ export class DataViewsService {
'Unable to write data view! Refresh the page to get the most up to date changes for this data view.',
});
- this.onNotification({ title, color: 'danger' });
+ this.onNotification(
+ { title, color: 'danger' },
+ `updateSavedObject:${indexPattern.title}`
+ );
throw err;
}
diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts
index 4032d83b24c5a1..f4bed383d8447f 100644
--- a/src/plugins/data_views/common/types.ts
+++ b/src/plugins/data_views/common/types.ts
@@ -123,8 +123,8 @@ export interface FieldAttrSet {
count?: number;
}
-export type OnNotification = (toastInputFields: ToastInputFields) => void;
-export type OnError = (error: Error, toastInputFields: ErrorToastOptions) => void;
+export type OnNotification = (toastInputFields: ToastInputFields, key: string) => void;
+export type OnError = (error: Error, toastInputFields: ErrorToastOptions, key: string) => void;
export interface UiSettingsCommon {
get: (key: string) => Promise;
diff --git a/src/plugins/data_views/public/debounce_by_key.test.ts b/src/plugins/data_views/public/debounce_by_key.test.ts
new file mode 100644
index 00000000000000..c5fba82fdcfdf9
--- /dev/null
+++ b/src/plugins/data_views/public/debounce_by_key.test.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { debounceByKey } from './debounce_by_key';
+
+describe('debounceByKey', () => {
+ test('debounce, confirm params', async () => {
+ const fn = jest.fn();
+ const fn2 = jest.fn();
+
+ const debouncedFn = debounceByKey(fn, 1000);
+ const debouncedFn2 = debounceByKey(fn2, 1000);
+
+ // debounces based on key, not params
+ debouncedFn('a')(1);
+ debouncedFn('a')(2);
+
+ debouncedFn2('b')(2);
+ debouncedFn2('b')(1);
+
+ expect(fn).toBeCalledTimes(1);
+ expect(fn).toBeCalledWith(1);
+ expect(fn2).toBeCalledTimes(1);
+ expect(fn2).toBeCalledWith(2);
+ });
+});
diff --git a/src/plugins/data_views/public/debounce_by_key.ts b/src/plugins/data_views/public/debounce_by_key.ts
new file mode 100644
index 00000000000000..c8ae7094a6437b
--- /dev/null
+++ b/src/plugins/data_views/public/debounce_by_key.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { debounce } from 'lodash';
+
+export const debounceByKey = any>(
+ fn: F,
+ waitInMs: number
+): ((key: string) => F) => {
+ const debouncerCollector: Record = {};
+ return (key: string) => {
+ if (!debouncerCollector[key]) {
+ debouncerCollector[key] = debounce(fn, waitInMs, {
+ leading: true,
+ });
+ }
+ return debouncerCollector[key];
+ };
+};
diff --git a/src/plugins/data_views/public/plugin.ts b/src/plugins/data_views/public/plugin.ts
index 95378df130c469..5c3ad2c33307dd 100644
--- a/src/plugins/data_views/public/plugin.ts
+++ b/src/plugins/data_views/public/plugin.ts
@@ -25,6 +25,8 @@ import {
import { DataViewsServicePublic } from './data_views_service_public';
import { HasData } from './services';
+import { debounceByKey } from './debounce_by_key';
+
export class DataViewsPublicPlugin
implements
Plugin<
@@ -50,16 +52,28 @@ export class DataViewsPublicPlugin
{ fieldFormats }: DataViewsPublicStartDependencies
): DataViewsPublicPluginStart {
const { uiSettings, http, notifications, savedObjects, theme, overlays, application } = core;
+
+ const onNotifDebounced = debounceByKey(
+ notifications.toasts.add.bind(notifications.toasts),
+ 10000
+ );
+ const onErrorDebounced = debounceByKey(
+ notifications.toasts.addError.bind(notifications.toasts),
+ 10000
+ );
+
return new DataViewsServicePublic({
hasData: this.hasData.start(core),
uiSettings: new UiSettingsPublicToCommon(uiSettings),
savedObjectsClient: new SavedObjectsClientPublicToCommon(savedObjects.client),
apiClient: new DataViewsApiClient(http),
fieldFormats,
- onNotification: (toastInputFields) => {
- notifications.toasts.add(toastInputFields);
+ onNotification: (toastInputFields, key) => {
+ onNotifDebounced(key)(toastInputFields);
+ },
+ onError: (error, toastInputFields, key) => {
+ onErrorDebounced(key)(error, toastInputFields);
},
- onError: notifications.toasts.addError.bind(notifications.toasts),
onRedirectNoIndexPattern: onRedirectNoIndexPattern(
application.capabilities,
application.navigateToApp,
diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts
index 173264aee731ee..2ea0215cad04d0 100644
--- a/src/plugins/discover/common/index.ts
+++ b/src/plugins/discover/common/index.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+export const PLUGIN_ID = 'discover';
export const APP_ICON = 'discoverApp';
export const DEFAULT_COLUMNS_SETTING = 'defaultColumns';
export const SAMPLE_SIZE_SETTING = 'discover:sampleSize';
diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx
index 5bf5107dc03494..44dcb0901dd7cf 100644
--- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx
+++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx
@@ -7,7 +7,7 @@
*/
import React from 'react';
-import { mountWithIntl } from '@kbn/test-jest-helpers';
+import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import {
CALLOUT_STATE_KEY,
@@ -15,11 +15,12 @@ import {
} from './document_explorer_update_callout';
import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock';
import { DiscoverServices } from '../../../../build_services';
+import { discoverServiceMock } from '../../../../__mocks__/services';
+import { DiscoverTourProvider } from '../../../../components/discover_tour';
const defaultServices = {
- addBasePath: () => '',
- docLinks: { links: { discover: { documentExplorer: '' } } },
- capabilities: { advancedSettings: { save: true } },
+ ...discoverServiceMock,
+ capabilities: { ...discoverServiceMock.capabilities, advancedSettings: { save: true } },
storage: new LocalStorageMock({ [CALLOUT_STATE_KEY]: false }),
} as unknown as DiscoverServices;
@@ -57,4 +58,18 @@ describe('Document Explorer Update callout', () => {
expect(result.find('.dscDocumentExplorerCallout').exists()).toBeFalsy();
});
+
+ it('should start a tour when the button is clicked', () => {
+ const result = mountWithIntl(
+
+
+
+
+
+ );
+
+ expect(result.find({ isStepOpen: true })).toHaveLength(0);
+ findTestSubject(result, 'discoverTakeTourButton').simulate('click');
+ expect(result.find({ isStepOpen: true })).toHaveLength(1);
+ });
});
diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx
index 5a9c6a68d6bb3b..2fe073946627ad 100644
--- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx
+++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx
@@ -6,22 +6,21 @@
* Side Public License, v 1.
*/
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback, useState } from 'react';
import './document_explorer_callout.scss';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
+ EuiButtonEmpty,
EuiButtonIcon,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
- EuiLink,
- useEuiTheme,
} from '@elastic/eui';
-import { css } from '@emotion/react';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { useDiscoverServices } from '../../../../utils/use_discover_services';
+import { useDiscoverTourContext } from '../../../../components/discover_tour';
export const CALLOUT_STATE_KEY = 'discover:docExplorerUpdateCalloutClosed';
@@ -37,16 +36,9 @@ const updateStoredCalloutState = (newState: boolean, storage: Storage) => {
* The callout that's displayed when Document explorer is enabled
*/
export const DocumentExplorerUpdateCallout = () => {
- const { euiTheme } = useEuiTheme();
- const { storage, capabilities, docLinks } = useDiscoverServices();
+ const { storage, capabilities } = useDiscoverServices();
const [calloutClosed, setCalloutClosed] = useState(getStoredCalloutState(storage));
-
- const semiBoldStyle = useMemo(
- () => css`
- font-weight: ${euiTheme.font.weight.semiBold};
- `,
- [euiTheme.font.weight.semiBold]
- );
+ const { onStartTour } = useDiscoverTourContext();
const onCloseCallout = useCallback(() => {
updateStoredCalloutState(true, storage);
@@ -67,44 +59,37 @@ export const DocumentExplorerUpdateCallout = () => {
>
-
-
-
-
- ),
- documentExplorer: (
-
-
-
-
-
- ),
- }}
+ id="discover.docExplorerUpdateCallout.description"
+ defaultMessage="Add relevant fields, reorder and sort columns, resize rows, and more in the document table."
/>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
};
@@ -114,8 +99,8 @@ function CalloutTitle({ onCloseCallout }: { onCloseCallout: () => void }) {
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
index 7b715bb56a74c7..1a84516fbdd8d6 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
@@ -35,6 +35,7 @@ import { SortPairArr } from '../../../../components/doc_table/lib/get_sort';
import { ElasticSearchHit } from '../../../../types';
import { DocumentExplorerCallout } from '../document_explorer_callout';
import { DocumentExplorerUpdateCallout } from '../document_explorer_callout/document_explorer_update_callout';
+import { DiscoverTourProvider } from '../../../../components/discover_tour';
const DocTableInfiniteMemoized = React.memo(DocTableInfinite);
const DataGridMemoized = React.memo(DiscoverGrid);
@@ -157,7 +158,9 @@ function DiscoverDocumentsComponent({
)}
{!isLegacy && (
<>
-
+
+
+
-
+
setIsFlyoutVisible(true)}
>
diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx
index 0361cf1764419e..c69cb448a195ca 100644
--- a/src/plugins/discover/public/application/main/discover_main_route.tsx
+++ b/src/plugins/discover/public/application/main/discover_main_route.tsx
@@ -15,6 +15,10 @@ import {
} from '@kbn/data-views-plugin/public';
import { redirectWhenMissing } from '@kbn/kibana-utils-plugin/public';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
+import {
+ AnalyticsNoDataPageKibanaProvider,
+ AnalyticsNoDataPage,
+} from '@kbn/shared-ux-page-analytics-no-data';
import {
SavedSearch,
getSavedSearch,
@@ -45,6 +49,7 @@ export function DiscoverMainRoute() {
data,
toastNotifications,
http: { basePath },
+ dataViewEditor,
} = services;
const [error, setError] = useState();
const [savedSearch, setSavedSearch] = useState();
@@ -52,6 +57,7 @@ export function DiscoverMainRoute() {
const [indexPatternList, setIndexPatternList] = useState>>(
[]
);
+ const [showNoDataPage, setShowNoDataPage] = useState(false);
const { id } = useParams();
useExecutionContext(core.executionContext, {
@@ -60,27 +66,20 @@ export function DiscoverMainRoute() {
id: id || 'new',
});
- const navigateToOverview = useCallback(() => {
- core.application.navigateToApp('kibanaOverview', { path: '#' });
- }, [core.application]);
-
- const checkForDataViews = useCallback(async () => {
- const hasUserDataView = await data.dataViews.hasUserDataView().catch(() => true);
- if (!hasUserDataView) {
- navigateToOverview();
- }
- const defaultDataView = await data.dataViews.getDefaultDataView();
- if (!defaultDataView) {
- navigateToOverview();
- }
- }, [navigateToOverview, data.dataViews]);
-
- useEffect(() => {
- const savedSearchId = id;
-
- async function loadDefaultOrCurrentIndexPattern(searchSource: ISearchSource) {
+ const loadDefaultOrCurrentIndexPattern = useCallback(
+ async (searchSource: ISearchSource) => {
try {
- await checkForDataViews();
+ const hasUserDataView = await data.dataViews.hasData.hasUserDataView().catch(() => false);
+ const hasEsData = await data.dataViews.hasData.hasESData().catch(() => false);
+ if (!hasUserDataView || !hasEsData) {
+ setShowNoDataPage(true);
+ return;
+ }
+ const defaultDataView = await data.dataViews.getDefaultDataView();
+ if (!defaultDataView) {
+ setShowNoDataPage(true);
+ return;
+ }
const { appStateContainer } = getState({ history, uiSettings: config });
const { index } = appStateContainer.getState();
const ip = await loadIndexPattern(index || '', data.dataViews, config);
@@ -94,78 +93,91 @@ export function DiscoverMainRoute() {
} catch (e) {
setError(e);
}
- }
+ },
+ [config, data.dataViews, history, toastNotifications]
+ );
- async function loadSavedSearch() {
- try {
- const currentSavedSearch = await getSavedSearch(savedSearchId, {
- search: services.data.search,
- savedObjectsClient: core.savedObjects.client,
- spaces: services.spaces,
- });
-
- const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern(
- currentSavedSearch.searchSource
- );
+ const loadSavedSearch = useCallback(async () => {
+ try {
+ const currentSavedSearch = await getSavedSearch(id, {
+ search: services.data.search,
+ savedObjectsClient: core.savedObjects.client,
+ spaces: services.spaces,
+ });
- if (!loadedIndexPattern) {
- return;
- }
+ const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern(
+ currentSavedSearch.searchSource
+ );
- if (!currentSavedSearch.searchSource.getField('index')) {
- currentSavedSearch.searchSource.setField('index', loadedIndexPattern);
- }
+ if (!loadedIndexPattern) {
+ return;
+ }
- setSavedSearch(currentSavedSearch);
+ if (!currentSavedSearch.searchSource.getField('index')) {
+ currentSavedSearch.searchSource.setField('index', loadedIndexPattern);
+ }
- if (currentSavedSearch.id) {
- chrome.recentlyAccessed.add(
- getSavedSearchFullPathUrl(currentSavedSearch.id),
- currentSavedSearch.title ?? '',
- currentSavedSearch.id
- );
- }
- } catch (e) {
- if (e instanceof DataViewSavedObjectConflictError) {
- setError(e);
- } else {
- redirectWhenMissing({
- history,
- navigateToApp: core.application.navigateToApp,
- basePath,
- mapping: {
- search: '/',
- 'index-pattern': {
- app: 'management',
- path: `kibana/objects/savedSearches/${id}`,
- },
- },
- toastNotifications,
- onBeforeRedirect() {
- getUrlTracker().setTrackedUrl('/');
+ setSavedSearch(currentSavedSearch);
+
+ if (currentSavedSearch.id) {
+ chrome.recentlyAccessed.add(
+ getSavedSearchFullPathUrl(currentSavedSearch.id),
+ currentSavedSearch.title ?? '',
+ currentSavedSearch.id
+ );
+ }
+ } catch (e) {
+ if (e instanceof DataViewSavedObjectConflictError) {
+ setError(e);
+ } else {
+ redirectWhenMissing({
+ history,
+ navigateToApp: core.application.navigateToApp,
+ basePath,
+ mapping: {
+ search: '/',
+ 'index-pattern': {
+ app: 'management',
+ path: `kibana/objects/savedSearches/${id}`,
},
- theme: core.theme,
- })(e);
- }
+ },
+ toastNotifications,
+ onBeforeRedirect() {
+ getUrlTracker().setTrackedUrl('/');
+ },
+ theme: core.theme,
+ })(e);
}
}
-
- loadSavedSearch();
}, [
+ id,
+ services.data.search,
+ services.spaces,
core.savedObjects.client,
- basePath,
- chrome.recentlyAccessed,
- config,
core.application.navigateToApp,
- data.dataViews,
+ core.theme,
+ loadDefaultOrCurrentIndexPattern,
+ chrome.recentlyAccessed,
history,
- id,
- services,
+ basePath,
toastNotifications,
- core.theme,
- checkForDataViews,
]);
+ const onDataViewCreated = useCallback(
+ async (dataView: unknown) => {
+ if (dataView) {
+ setShowNoDataPage(false);
+ setError(undefined);
+ await loadSavedSearch();
+ }
+ },
+ [loadSavedSearch]
+ );
+
+ useEffect(() => {
+ loadSavedSearch();
+ }, [loadSavedSearch]);
+
useEffect(() => {
chrome.setBreadcrumbs(
savedSearch && savedSearch.title
@@ -174,6 +186,19 @@ export function DiscoverMainRoute() {
);
}, [chrome, savedSearch]);
+ if (showNoDataPage) {
+ const analyticsServices = {
+ coreStart: core,
+ dataViews: data.dataViews,
+ dataViewEditor,
+ };
+ return (
+
+
+
+ );
+ }
+
if (error) {
return ;
}
diff --git a/src/plugins/discover/public/assets/discover_tour/add_fields.gif b/src/plugins/discover/public/assets/discover_tour/add_fields.gif
new file mode 100644
index 00000000000000..c955a9aa99e08d
Binary files /dev/null and b/src/plugins/discover/public/assets/discover_tour/add_fields.gif differ
diff --git a/src/plugins/discover/public/assets/discover_tour/expand.gif b/src/plugins/discover/public/assets/discover_tour/expand.gif
new file mode 100644
index 00000000000000..7131fed839478d
Binary files /dev/null and b/src/plugins/discover/public/assets/discover_tour/expand.gif differ
diff --git a/src/plugins/discover/public/assets/discover_tour/reorder_columns.gif b/src/plugins/discover/public/assets/discover_tour/reorder_columns.gif
new file mode 100644
index 00000000000000..d3aeedb513c1ee
Binary files /dev/null and b/src/plugins/discover/public/assets/discover_tour/reorder_columns.gif differ
diff --git a/src/plugins/discover/public/assets/discover_tour/rows_per_line.gif b/src/plugins/discover/public/assets/discover_tour/rows_per_line.gif
new file mode 100644
index 00000000000000..66033d03d8fd28
Binary files /dev/null and b/src/plugins/discover/public/assets/discover_tour/rows_per_line.gif differ
diff --git a/src/plugins/discover/public/assets/discover_tour/sort.gif b/src/plugins/discover/public/assets/discover_tour/sort.gif
new file mode 100644
index 00000000000000..6d22b947a206f1
Binary files /dev/null and b/src/plugins/discover/public/assets/discover_tour/sort.gif differ
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx
index da9892f343d706..64cbab5c1511b2 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx
@@ -6,12 +6,11 @@
* Side Public License, v 1.
*/
-import React, { useCallback, useMemo, useState } from 'react';
+import React, { useCallback, useMemo, useState, useRef } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import './discover_grid.scss';
import {
EuiDataGridSorting,
- EuiDataGridProps,
EuiDataGrid,
EuiScreenReaderOnly,
EuiSpacer,
@@ -19,6 +18,7 @@ import {
htmlIdGenerator,
EuiLoadingSpinner,
EuiIcon,
+ EuiDataGridRefProps,
} from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
import { flattenHit } from '@kbn/data-plugin/public';
@@ -165,9 +165,7 @@ export interface DiscoverGridProps {
onUpdateRowHeight?: (rowHeight: number) => void;
}
-export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => {
- return ;
-});
+export const EuiDataGridMemoized = React.memo(EuiDataGrid);
const CONTROL_COLUMN_IDS_DEFAULT = ['openDetails', 'select'];
@@ -199,6 +197,7 @@ export const DiscoverGrid = ({
rowHeightState,
onUpdateRowHeight,
}: DiscoverGridProps) => {
+ const dataGridRef = useRef(null);
const services = useDiscoverServices();
const [selectedDocs, setSelectedDocs] = useState([]);
const [isFilterActive, setIsFilterActive] = useState(false);
@@ -232,6 +231,12 @@ export const DiscoverGrid = ({
return rowsFiltered;
}, [rows, usedSelectedDocs, isFilterActive]);
+ const displayedRowsFlattened = useMemo(() => {
+ return displayedRows.map((hit) => {
+ return flattenHit(hit, indexPattern, { includeIgnoredValues: true });
+ });
+ }, [displayedRows, indexPattern]);
+
/**
* Pagination
*/
@@ -290,16 +295,20 @@ export const DiscoverGrid = ({
getRenderCellValueFn(
indexPattern,
displayedRows,
- displayedRows
- ? displayedRows.map((hit) =>
- flattenHit(hit, indexPattern, { includeIgnoredValues: true })
- )
- : [],
+ displayedRowsFlattened,
useNewFieldsApi,
fieldsToShow,
- services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED)
+ services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED),
+ () => dataGridRef.current?.closeCellPopover()
),
- [indexPattern, displayedRows, useNewFieldsApi, fieldsToShow, services.uiSettings]
+ [
+ indexPattern,
+ displayedRowsFlattened,
+ displayedRows,
+ useNewFieldsApi,
+ fieldsToShow,
+ services.uiSettings,
+ ]
);
/**
@@ -432,6 +441,7 @@ export const DiscoverGrid = ({
expanded: expandedDoc,
setExpanded: setExpandedDoc,
rows: displayedRows,
+ rowsFlattened: displayedRowsFlattened,
onFilter,
indexPattern,
isDarkMode: services.uiSettings.get('theme:darkMode'),
@@ -463,6 +473,7 @@ export const DiscoverGrid = ({
onColumnResize={onResize}
pagination={paginationObj}
renderCellValue={renderCellValue}
+ ref={dataGridRef}
rowCount={rowCount}
schemaDetectors={schemaDetectors}
sorting={sorting as EuiDataGridSorting}
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx
index 9a75a74396ff05..5ce0befcf93050 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx
@@ -5,17 +5,50 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+const mockCopyToClipboard = jest.fn();
+jest.mock('@elastic/eui', () => {
+ const original = jest.requireActual('@elastic/eui');
+ return {
+ ...original,
+ copyToClipboard: (value: string) => mockCopyToClipboard(value),
+ };
+});
+
+jest.mock('../../utils/use_discover_services', () => {
+ const services = {
+ toastNotifications: {
+ addInfo: jest.fn(),
+ },
+ };
+ const originalModule = jest.requireActual('../../utils/use_discover_services');
+ return {
+ ...originalModule,
+ useDiscoverServices: () => services,
+ };
+});
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
-import { FilterInBtn, FilterOutBtn, buildCellActions } from './discover_grid_cell_actions';
+import { FilterInBtn, FilterOutBtn, buildCellActions, CopyBtn } from './discover_grid_cell_actions';
import { DiscoverGridContext } from './discover_grid_context';
-
+import { EuiButton } from '@elastic/eui';
import { indexPatternMock } from '../../__mocks__/index_pattern';
import { esHits } from '../../__mocks__/es_hits';
-import { EuiButton } from '@elastic/eui';
import { DataViewField } from '@kbn/data-views-plugin/public';
+import { flattenHit } from '@kbn/data-plugin/common';
+
+const contextMock = {
+ expanded: undefined,
+ setExpanded: jest.fn(),
+ rows: esHits,
+ rowsFlattened: esHits.map((hit) => flattenHit(hit, indexPatternMock)),
+ onFilter: jest.fn(),
+ indexPattern: indexPatternMock,
+ isDarkMode: false,
+ selectedDocs: [],
+ setSelectedDocs: jest.fn(),
+};
describe('Discover cell actions ', function () {
it('should not show cell actions for unfilterable fields', async () => {
@@ -23,17 +56,6 @@ describe('Discover cell actions ', function () {
});
it('triggers filter function when FilterInBtn is clicked', async () => {
- const contextMock = {
- expanded: undefined,
- setExpanded: jest.fn(),
- rows: esHits,
- onFilter: jest.fn(),
- indexPattern: indexPatternMock,
- isDarkMode: false,
- selectedDocs: [],
- setSelectedDocs: jest.fn(),
- };
-
const component = mountWithIntl(
{
- const contextMock = {
- expanded: undefined,
- setExpanded: jest.fn(),
- rows: esHits,
- onFilter: jest.fn(),
- indexPattern: indexPatternMock,
- isDarkMode: false,
- selectedDocs: [],
- setSelectedDocs: jest.fn(),
- };
-
const component = mountWithIntl(
{
+ const component = mountWithIntl(
+
+ }
+ rowIndex={1}
+ colIndex={1}
+ columnId="extension"
+ isExpanded={false}
+ />
+
+ );
+ const button = findTestSubject(component, 'copyClipboardButton');
+ await button.simulate('click');
+ expect(mockCopyToClipboard).toHaveBeenCalledWith('jpg');
+ });
});
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx
index 318e1719c08f89..df07478dae5c6c 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx
@@ -7,11 +7,12 @@
*/
import React, { useContext } from 'react';
-import { EuiDataGridColumnCellActionProps } from '@elastic/eui';
+import { copyToClipboard, EuiDataGridColumnCellActionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DataViewField } from '@kbn/data-views-plugin/public';
-import { flattenHit } from '@kbn/data-plugin/public';
import { DiscoverGridContext, GridContext } from './discover_grid_context';
+import { useDiscoverServices } from '../../utils/use_discover_services';
+import { formatFieldValue } from '../../utils/format_value';
function onFilterCell(
context: GridContext,
@@ -19,12 +20,12 @@ function onFilterCell(
columnId: EuiDataGridColumnCellActionProps['columnId'],
mode: '+' | '-'
) {
- const row = context.rows[rowIndex];
- const flattened = flattenHit(row, context.indexPattern);
+ const row = context.rowsFlattened[rowIndex];
+ const value = String(row[columnId]);
const field = context.indexPattern.fields.getByName(columnId);
- if (flattened && field) {
- context.onFilter(field, flattened[columnId], mode);
+ if (value && field) {
+ context.onFilter(field, value, mode);
}
}
@@ -84,8 +85,52 @@ export const FilterOutBtn = ({
);
};
+export const CopyBtn = ({ Component, rowIndex, columnId }: EuiDataGridColumnCellActionProps) => {
+ const { indexPattern: dataView, rowsFlattened, rows } = useContext(DiscoverGridContext);
+ const { fieldFormats, toastNotifications } = useDiscoverServices();
+
+ const buttonTitle = i18n.translate('discover.grid.copyClipboardButtonTitle', {
+ defaultMessage: 'Copy value of {column}',
+ values: { column: columnId },
+ });
+
+ return (
+ {
+ const rowFlattened = rowsFlattened[rowIndex];
+ const field = dataView.fields.getByName(columnId);
+ const value = rowFlattened[columnId];
+
+ const valueFormatted =
+ field?.type === '_source'
+ ? JSON.stringify(rowFlattened, null, 2)
+ : formatFieldValue(value, rows[rowIndex], fieldFormats, dataView, field, 'text');
+ copyToClipboard(valueFormatted);
+ const infoTitle = i18n.translate('discover.grid.copyClipboardToastTitle', {
+ defaultMessage: 'Copied value of {column} to clipboard.',
+ values: { column: columnId },
+ });
+
+ toastNotifications.addInfo({
+ title: infoTitle,
+ });
+ }}
+ iconType="copyClipboard"
+ aria-label={buttonTitle}
+ title={buttonTitle}
+ data-test-subj="copyClipboardButton"
+ >
+ {i18n.translate('discover.grid.copyClipboardButton', {
+ defaultMessage: 'Copy to clipboard',
+ })}
+
+ );
+};
+
export function buildCellActions(field: DataViewField) {
- if (!field.filterable) {
+ if (field?.type === '_source') {
+ return [CopyBtn];
+ } else if (!field.filterable) {
return undefined;
}
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx
index 41d58cf2133361..f1b21dabab86ee 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx
@@ -15,6 +15,7 @@ export interface GridContext {
expanded?: ElasticSearchHit;
setExpanded: (hit?: ElasticSearchHit) => void;
rows: ElasticSearchHit[];
+ rowsFlattened: Array>;
onFilter: DocViewFilterFn;
indexPattern: DataView;
isDarkMode: boolean;
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx
index d416372ac183fd..f1d8ab9fcb86df 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_document_selection.test.tsx
@@ -21,6 +21,7 @@ const baseContextMock = {
expanded: undefined,
setExpanded: jest.fn(),
rows: esHits,
+ rowsFlattened: esHits,
onFilter: jest.fn(),
indexPattern: indexPatternMock,
isDarkMode: false,
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx
index 27ee307d746ebf..903d0bc4bedcd4 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.test.tsx
@@ -18,6 +18,7 @@ const baseContextMock = {
expanded: undefined,
setExpanded: jest.fn(),
rows: esHits,
+ rowsFlattened: esHits,
onFilter: jest.fn(),
indexPattern: indexPatternMock,
isDarkMode: false,
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx
index 6765a8d24f91a6..a64d8521f503e6 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx
@@ -12,6 +12,8 @@ import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-th
import { i18n } from '@kbn/i18n';
import { DiscoverGridContext } from './discover_grid_context';
import { EsHitRecord } from '../../application/types';
+import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../discover_tour';
+
/**
* Button to expand a given row
*/
@@ -42,6 +44,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
return (
key === 'discover:maxDocFieldsDisplayed' && 200,
+ },
+ fieldFormats: {
+ getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => (value ? value : '-') })),
+ },
+};
jest.mock('../../utils/use_discover_services', () => {
- const services = {
- uiSettings: {
- get: (key: string) => key === 'discover:maxDocFieldsDisplayed' && 200,
- },
- fieldFormats: {
- getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => (value ? value : '-') })),
- },
- };
const originalModule = jest.requireActual('../../utils/use_discover_services');
return {
...originalModule,
- useDiscoverServices: () => services,
+ useDiscoverServices: () => mockServices,
};
});
@@ -79,7 +83,8 @@ describe('Discover grid cell rendering', function () {
rowsSource.map(flatten),
false,
[],
- 100
+ 100,
+ jest.fn()
);
const component = shallow(
);
expect(component.html()).toMatchInlineSnapshot(
- `"100"`
+ `""`
);
});
it('renders bytes column correctly using fields when details is true', () => {
+ const closePopoverMockFn = jest.fn();
const DiscoverGridCellValue = getRenderCellValueFn(
indexPatternMock,
rowsFields,
rowsFields.map(flatten),
false,
[],
- 100
+ 100,
+ closePopoverMockFn
);
- const component = shallow(
+ const component = mountWithIntl(
);
expect(component.html()).toMatchInlineSnapshot(
- `"100"`
+ `""`
);
+ findTestSubject(component, 'docTableClosePopover').simulate('click');
+ expect(closePopoverMockFn).toHaveBeenCalledTimes(1);
});
it('renders _source column correctly', () => {
@@ -154,7 +164,8 @@ describe('Discover grid cell rendering', function () {
rowsSource.map(flatten),
false,
['extension', 'bytes'],
- 100
+ 100,
+ jest.fn()
);
const component = shallow(
);
expect(component).toMatchInlineSnapshot(`
-
+
+
+
+
+
+
+
+
+
+
+
+
`);
});
@@ -271,7 +313,8 @@ describe('Discover grid cell rendering', function () {
rowsFields.map(flatten),
true,
['extension', 'bytes'],
- 100
+ 100,
+ jest.fn()
);
const component = shallow(
);
expect(component).toMatchInlineSnapshot(`
-
+
+
+
+
+
+
+
+
+
+
+
+
`);
});
@@ -476,7 +551,8 @@ describe('Discover grid cell rendering', function () {
rowsFieldsWithTopLevelObject.map(flatten),
true,
['object.value', 'extension', 'bytes'],
- 100
+ 100,
+ jest.fn()
);
const component = shallow(
{
+ const closePopoverMockFn = jest.fn();
const DiscoverGridCellValue = getRenderCellValueFn(
indexPatternMock,
rowsFieldsWithTopLevelObject,
rowsFieldsWithTopLevelObject.map(flatten),
true,
[],
- 100
+ 100,
+ closePopoverMockFn
);
const component = shallow(
);
expect(component).toMatchInlineSnapshot(`
-
+
+
+
+
+
+
+
+
+
+
+
+
`);
});
+ it('renders a functional close button when CodeEditor is rendered', () => {
+ const closePopoverMockFn = jest.fn();
+ const DiscoverGridCellValue = getRenderCellValueFn(
+ indexPatternMock,
+ rowsFieldsWithTopLevelObject,
+ rowsFieldsWithTopLevelObject.map(flatten),
+ true,
+ [],
+ 100,
+ closePopoverMockFn
+ );
+ const component = mountWithIntl(
+
+
+
+ );
+ const gridSelectionBtn = findTestSubject(component, 'docTableClosePopover');
+ gridSelectionBtn.simulate('click');
+ expect(closePopoverMockFn).toHaveBeenCalledTimes(1);
+ });
+
it('does not collect subfields when the the column is unmapped but part of fields response', () => {
(indexPatternMock.getFieldByName as jest.Mock).mockReturnValueOnce(undefined);
const DiscoverGridCellValue = getRenderCellValueFn(
@@ -594,7 +732,8 @@ describe('Discover grid cell rendering', function () {
rowsFieldsWithTopLevelObject.map(flatten),
true,
[],
- 100
+ 100,
+ jest.fn()
);
const component = shallow(
);
expect(componentWithDetails).toMatchInlineSnapshot(`
-
+
+
+
+
+
+
+
+
`);
});
});
diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx
index b6a63d47b7a0f6..4175ff1bdd7b5a 100644
--- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx
+++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx
@@ -8,6 +8,7 @@
import React, { Fragment, useContext, useEffect, useMemo } from 'react';
import classnames from 'classnames';
+import { i18n } from '@kbn/i18n';
import { euiLightVars as themeLight, euiDarkVars as themeDark } from '@kbn/ui-theme';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import {
@@ -15,6 +16,9 @@ import {
EuiDescriptionList,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
} from '@elastic/eui';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { DiscoverGridContext } from './discover_grid_context';
@@ -36,7 +40,8 @@ export const getRenderCellValueFn =
rowsFlattened: Array>,
useNewFieldsApi: boolean,
fieldsToShow: string[],
- maxDocFieldsDisplayed: number
+ maxDocFieldsDisplayed: number,
+ closePopover: () => void
) =>
({ rowIndex, columnId, isDetails, setCellProps }: EuiDataGridCellValueElementProps) => {
const { uiSettings, fieldFormats } = useDiscoverServices();
@@ -93,6 +98,7 @@ export const getRenderCellValueFn =
dataView,
useTopLevelObjectColumns,
fieldFormats,
+ closePopover,
});
}
@@ -147,6 +153,13 @@ function getInnerColumns(fields: Record, columnId: string) {
);
}
+function getJSON(columnId: string, rowRaw: ElasticSearchHit, useTopLevelObjectColumns: boolean) {
+ const json = useTopLevelObjectColumns
+ ? getInnerColumns(rowRaw.fields as Record, columnId)
+ : rowRaw;
+ return json as Record;
+}
+
/**
* Helper function for the cell popover
*/
@@ -158,6 +171,7 @@ function renderPopoverContent({
dataView,
useTopLevelObjectColumns,
fieldFormats,
+ closePopover,
}: {
rowRaw: ElasticSearchHit;
rowFlattened: Record;
@@ -166,25 +180,53 @@ function renderPopoverContent({
dataView: DataView;
useTopLevelObjectColumns: boolean;
fieldFormats: FieldFormatsStart;
+ closePopover: () => void;
}) {
+ const closeButton = (
+
+ );
if (useTopLevelObjectColumns || field?.type === '_source') {
- const json = useTopLevelObjectColumns
- ? getInnerColumns(rowRaw.fields as Record, columnId)
- : rowRaw;
return (
- } width={defaultMonacoEditorWidth} />
+
+
+
+ {closeButton}
+
+
+
+
+
+
);
}
return (
-
+
+
+
+
+ {closeButton}
+
);
}
/**
diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour.test.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour.test.tsx
new file mode 100644
index 00000000000000..cc25819443d22c
--- /dev/null
+++ b/src/plugins/discover/public/components/discover_tour/discover_tour.test.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiTourStep, EuiButton } from '@elastic/eui';
+import { mountWithIntl } from '@kbn/test-jest-helpers';
+import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
+import { discoverServiceMock } from '../../__mocks__/services';
+import { DiscoverTourProvider } from './discover_tour_provider';
+import { useDiscoverTourContext } from './discover_tour_context';
+import { DISCOVER_TOUR_STEP_ANCHORS } from './discover_tour_anchors';
+
+describe('Discover tour', () => {
+ const mountComponent = (innerContent?: JSX.Element) => {
+ return mountWithIntl(
+
+ {innerContent}
+
+ );
+ };
+
+ it('should start successfully', () => {
+ const buttonSubjToTestStart = 'discoverTourButtonTestStart';
+ const InnerComponent = () => {
+ const tourContext = useDiscoverTourContext();
+
+ return (
+
+ {'Start the tour'}
+
+ );
+ };
+
+ const component = mountComponent();
+ // all steps are hidden by default
+ expect(component.find(EuiTourStep)).toHaveLength(0);
+
+ // one step should become visible after the tour is triggered
+ component.find(`[data-test-subj="${buttonSubjToTestStart}"]`).at(0).simulate('click');
+
+ expect(component.find(EuiTourStep)).toHaveLength(5);
+ expect(
+ component.find({ anchor: DISCOVER_TOUR_STEP_ANCHORS.addFields, isStepOpen: true })
+ ).toHaveLength(1);
+ expect(
+ component.find({ anchor: DISCOVER_TOUR_STEP_ANCHORS.reorderColumns, isStepOpen: false })
+ ).toHaveLength(1);
+ expect(
+ component.find({ anchor: DISCOVER_TOUR_STEP_ANCHORS.sort, isStepOpen: false })
+ ).toHaveLength(1);
+ expect(
+ component.find({ anchor: DISCOVER_TOUR_STEP_ANCHORS.changeRowHeight, isStepOpen: false })
+ ).toHaveLength(1);
+ expect(
+ component.find({ anchor: DISCOVER_TOUR_STEP_ANCHORS.expandDocument, isStepOpen: false })
+ ).toHaveLength(1);
+ });
+});
diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour_anchors.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour_anchors.tsx
new file mode 100644
index 00000000000000..030876291aeea3
--- /dev/null
+++ b/src/plugins/discover/public/components/discover_tour/discover_tour_anchors.tsx
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { htmlIdGenerator } from '@elastic/eui';
+
+export const DISCOVER_TOUR_STEP_ANCHOR_IDS = {
+ addFields: htmlIdGenerator('dsc-tour-step-add-fields')(),
+ expandDocument: htmlIdGenerator('dsc-tour-step-expand')(),
+};
+
+export const DISCOVER_TOUR_STEP_ANCHORS = {
+ addFields: `#${DISCOVER_TOUR_STEP_ANCHOR_IDS.addFields}`,
+ reorderColumns: '[data-test-subj="dataGridColumnSelectorButton"]',
+ sort: '[data-test-subj="dataGridColumnSortingButton"]',
+ changeRowHeight: '[data-test-subj="dataGridDisplaySelectorButton"]',
+ expandDocument: `#${DISCOVER_TOUR_STEP_ANCHOR_IDS.expandDocument}`,
+};
diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour_context.ts b/src/plugins/discover/public/components/discover_tour/discover_tour_context.ts
new file mode 100644
index 00000000000000..94bbfaf6555d2e
--- /dev/null
+++ b/src/plugins/discover/public/components/discover_tour/discover_tour_context.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createContext, useContext } from 'react';
+
+export interface DiscoverTourContextProps {
+ onStartTour: () => void;
+ onNextTourStep: () => void;
+ onFinishTour: () => void;
+}
+
+export const DiscoverTourContext = createContext({
+ onStartTour: () => {},
+ onNextTourStep: () => {},
+ onFinishTour: () => {},
+});
+
+export const useDiscoverTourContext = () => {
+ return useContext(DiscoverTourContext);
+};
diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx
new file mode 100644
index 00000000000000..610fc0907ea5a1
--- /dev/null
+++ b/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx
@@ -0,0 +1,306 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { useCallback, useMemo } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+ useEuiTour,
+ EuiTourState,
+ EuiTourStep,
+ EuiTourStepProps,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiButton,
+ EuiButtonProps,
+ EuiImage,
+ EuiSpacer,
+ EuiI18n,
+ EuiIcon,
+ EuiText,
+} from '@elastic/eui';
+import { PLUGIN_ID } from '../../../common';
+import { useDiscoverServices } from '../../utils/use_discover_services';
+import { DiscoverTourContext, DiscoverTourContextProps } from './discover_tour_context';
+import { DISCOVER_TOUR_STEP_ANCHORS } from './discover_tour_anchors';
+
+const MAX_WIDTH = 350;
+
+interface TourStepDefinition {
+ anchor: EuiTourStepProps['anchor'];
+ title: EuiTourStepProps['title'];
+ content: EuiTourStepProps['content'];
+ imageName: string;
+ imageAltText: string;
+}
+
+const tourStepDefinitions: TourStepDefinition[] = [
+ {
+ anchor: DISCOVER_TOUR_STEP_ANCHORS.addFields,
+ title: i18n.translate('discover.dscTour.stepAddFields.title', {
+ defaultMessage: 'Add fields to the table',
+ }),
+ content: (
+ ,
+ }}
+ />
+ ),
+ imageName: 'add_fields.gif',
+ imageAltText: i18n.translate('discover.dscTour.stepAddFields.imageAltText', {
+ defaultMessage:
+ 'In the Available fields list, click the plus icon to toggle a field into the document table.',
+ }),
+ },
+ {
+ anchor: DISCOVER_TOUR_STEP_ANCHORS.reorderColumns,
+ title: i18n.translate('discover.dscTour.stepReorderColumns.title', {
+ defaultMessage: 'Order the table columns',
+ }),
+ content: (
+
+ ),
+ imageName: 'reorder_columns.gif',
+ imageAltText: i18n.translate('discover.dscTour.stepReorderColumns.imageAltText', {
+ defaultMessage: 'Use the Columns popover to drag the columns to the order you prefer.',
+ }),
+ },
+ {
+ anchor: DISCOVER_TOUR_STEP_ANCHORS.sort,
+ title: i18n.translate('discover.dscTour.stepSort.title', {
+ defaultMessage: 'Sort on one or more fields',
+ }),
+ content: (
+
+ ),
+ imageName: 'sort.gif',
+ imageAltText: i18n.translate('discover.dscTour.stepSort.imageAltText', {
+ defaultMessage:
+ 'Click a column header and select the desired sort order. Adjust a multi-field sort using the fields sorted popover.',
+ }),
+ },
+ {
+ anchor: DISCOVER_TOUR_STEP_ANCHORS.changeRowHeight,
+ title: i18n.translate('discover.dscTour.stepChangeRowHeight.title', {
+ defaultMessage: 'Change the row height',
+ }),
+ content: (
+
+ ),
+ imageName: 'rows_per_line.gif',
+ imageAltText: i18n.translate('discover.dscTour.stepChangeRowHeight.imageAltText', {
+ defaultMessage:
+ 'Click the display options icon to adjust the row height to fit the contents.',
+ }),
+ },
+ {
+ anchor: DISCOVER_TOUR_STEP_ANCHORS.expandDocument,
+ title: i18n.translate('discover.dscTour.stepExpand.title', {
+ defaultMessage: 'Expand documents',
+ }),
+ content: (
+
+ ),
+ }}
+ />
+ ),
+ imageName: 'expand.gif',
+ imageAltText: i18n.translate('discover.dscTour.stepExpand.imageAltText', {
+ defaultMessage:
+ 'Click the expand icon to inspect and filter the fields in the document and view the document in context.',
+ }),
+ },
+];
+
+const FIRST_STEP = 1;
+
+const prepareTourSteps = (
+ stepDefinitions: TourStepDefinition[],
+ getAssetPath: (imageName: string) => string
+): EuiTourStepProps[] =>
+ stepDefinitions.map((stepDefinition, index) => ({
+ step: index + 1,
+ anchor: stepDefinition.anchor,
+ title: stepDefinition.title,
+ maxWidth: MAX_WIDTH,
+ content: (
+ <>
+
+ {stepDefinition.content}
+
+ {stepDefinition.imageName && (
+ <>
+
+
+ >
+ )}
+ >
+ ),
+ })) as EuiTourStepProps[];
+
+const findNextAvailableStep = (
+ steps: EuiTourStepProps[],
+ currentTourStep: number
+): number | null => {
+ const nextStep = steps.find(
+ (step) =>
+ step.step > currentTourStep &&
+ typeof step.anchor === 'string' &&
+ document.querySelector(step.anchor)
+ );
+
+ return nextStep?.step ?? null;
+};
+
+const tourConfig: EuiTourState = {
+ currentTourStep: FIRST_STEP,
+ isTourActive: false,
+ tourPopoverWidth: MAX_WIDTH,
+ tourSubtitle: '',
+};
+
+export const DiscoverTourProvider: React.FC = ({ children }) => {
+ const services = useDiscoverServices();
+ const prependToBasePath = services.core.http.basePath.prepend;
+ const getAssetPath = useCallback(
+ (imageName: string) => {
+ return prependToBasePath(`/plugins/${PLUGIN_ID}/assets/discover_tour/${imageName}`);
+ },
+ [prependToBasePath]
+ );
+ const tourSteps = useMemo(
+ () => prepareTourSteps(tourStepDefinitions, getAssetPath),
+ [getAssetPath]
+ );
+ const [steps, actions, reducerState] = useEuiTour(tourSteps, tourConfig);
+ const currentTourStep = reducerState.currentTourStep;
+ const isTourActive = reducerState.isTourActive;
+
+ const onStartTour = useCallback(() => {
+ actions.resetTour();
+ actions.goToStep(FIRST_STEP, true);
+ }, [actions]);
+
+ const onNextTourStep = useCallback(() => {
+ const nextAvailableStep = findNextAvailableStep(steps, currentTourStep);
+ if (nextAvailableStep) {
+ actions.goToStep(nextAvailableStep);
+ } else {
+ actions.finishTour();
+ }
+ }, [actions, steps, currentTourStep]);
+
+ const onFinishTour = useCallback(() => {
+ actions.finishTour();
+ }, [actions]);
+
+ const contextValue: DiscoverTourContextProps = useMemo(
+ () => ({
+ onStartTour,
+ onNextTourStep,
+ onFinishTour,
+ }),
+ [onStartTour, onNextTourStep, onFinishTour]
+ );
+
+ return (
+
+ {isTourActive &&
+ steps.map((step) => (
+
+ }
+ />
+ ))}
+ {children}
+
+ );
+};
+
+export const DiscoverTourStepFooterAction: React.FC<{
+ isLastStep: boolean;
+ onNextTourStep: DiscoverTourContextProps['onNextTourStep'];
+ onFinishTour: DiscoverTourContextProps['onFinishTour'];
+}> = ({ isLastStep, onNextTourStep, onFinishTour }) => {
+ const actionButtonProps: Partial = {
+ size: 's',
+ color: 'success',
+ };
+
+ return (
+
+ {!isLastStep && (
+
+
+ {EuiI18n({ token: 'core.euiTourStep.skipTour', default: 'Skip tour' })}
+
+
+ )}
+
+ {isLastStep ? (
+
+ {EuiI18n({ token: 'core.euiTourStep.endTour', default: 'End tour' })}
+
+ ) : (
+
+ {EuiI18n({ token: 'core.euiTourStep.nextStep', default: 'Next' })}
+
+ )}
+
+
+ );
+};
diff --git a/src/plugins/discover/public/components/discover_tour/index.ts b/src/plugins/discover/public/components/discover_tour/index.ts
new file mode 100644
index 00000000000000..b60c79a80f54d9
--- /dev/null
+++ b/src/plugins/discover/public/components/discover_tour/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { DiscoverTourProvider } from './discover_tour_provider';
+export { useDiscoverTourContext } from './discover_tour_context';
+export { DISCOVER_TOUR_STEP_ANCHOR_IDS, DISCOVER_TOUR_STEP_ANCHORS } from './discover_tour_anchors';
diff --git a/src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap
index 31dd6347218b5a..7af546298e0d86 100644
--- a/src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap
+++ b/src/plugins/discover/public/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap
@@ -2,6 +2,7 @@
exports[`returns the \`JsonCodeEditor\` component 1`] = `
;
width?: string | number;
+ height?: string | number;
hasLineNumbers?: boolean;
}
-export const JsonCodeEditor = ({ json, width, hasLineNumbers }: JsonCodeEditorProps) => {
+export const JsonCodeEditor = ({ json, width, height, hasLineNumbers }: JsonCodeEditorProps) => {
const jsonValue = JSON.stringify(json, null, 2);
- // setting editor height based on lines height and count to stretch and fit its content
- const setEditorCalculatedHeight = useCallback((editor) => {
- const editorElement = editor.getDomNode();
-
- if (!editorElement) {
- return;
- }
-
- const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight);
- const lineCount = editor.getModel()?.getLineCount() || 1;
- const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight;
-
- editorElement.style.height = `${height}px`;
- editor.layout();
- }, []);
-
return (
void 0}
+ hideCopyButton={true}
/>
);
};
diff --git a/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx b/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx
index 5f6faa8ac0e9d3..777240fe2f5bb0 100644
--- a/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx
+++ b/src/plugins/discover/public/components/json_code_editor/json_code_editor_common.tsx
@@ -27,6 +27,7 @@ interface JsonCodeEditorCommonProps {
width?: string | number;
height?: string | number;
hasLineNumbers?: boolean;
+ hideCopyButton?: boolean;
}
export const JsonCodeEditorCommon = ({
@@ -35,10 +36,40 @@ export const JsonCodeEditorCommon = ({
height,
hasLineNumbers,
onEditorDidMount,
+ hideCopyButton,
}: JsonCodeEditorCommonProps) => {
if (jsonValue === '') {
return null;
}
+ const codeEditor = (
+
+ );
+ if (hideCopyButton) {
+ return codeEditor;
+ }
return (
@@ -53,32 +84,7 @@ export const JsonCodeEditorCommon = ({
-
-
-
+ {codeEditor}
);
};
diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx
index 8330751cf13f04..b4a33119c23b29 100644
--- a/src/plugins/discover/public/plugin.tsx
+++ b/src/plugins/discover/public/plugin.tsx
@@ -35,6 +35,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
+import { PLUGIN_ID } from '../common';
import { DocViewInput, DocViewInputFn } from './services/doc_views/doc_views_types';
import { DocViewsRegistry } from './services/doc_views/doc_views_registry';
import {
@@ -257,7 +258,7 @@ export class DiscoverPlugin
};
core.application.register({
- id: 'discover',
+ id: PLUGIN_ID,
title: 'Discover',
updater$: this.appStateUpdater.asObservable(),
order: 1000,
diff --git a/src/plugins/discover/public/utils/format_value.ts b/src/plugins/discover/public/utils/format_value.ts
index 74331e946682e4..b7ee9af7f6873f 100644
--- a/src/plugins/discover/public/utils/format_value.ts
+++ b/src/plugins/discover/public/utils/format_value.ts
@@ -10,6 +10,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
+import { FieldFormatsContentType } from '@kbn/field-formats-plugin/common/types';
/**
* Formats the value of a specific field using the appropriate field formatter if available
@@ -26,16 +27,18 @@ export function formatFieldValue(
hit: estypes.SearchHit,
fieldFormats: FieldFormatsStart,
dataView?: DataView,
- field?: DataViewField
+ field?: DataViewField,
+ contentType?: FieldFormatsContentType | undefined
): string {
+ const usedContentType = contentType ?? 'html';
if (!dataView || !field) {
// If either no field is available or no data view, we'll use the default
// string formatter to format that field.
return fieldFormats
.getDefaultInstance(KBN_FIELD_TYPES.STRING)
- .convert(value, 'html', { hit, field });
+ .convert(value, usedContentType, { hit, field });
}
// If we have a data view and field we use that fields field formatter
- return dataView.getFormatterForField(field).convert(value, 'html', { hit, field });
+ return dataView.getFormatterForField(field).convert(value, usedContentType, { hit, field });
}
diff --git a/src/plugins/expressions/common/expression_functions/arguments.ts b/src/plugins/expressions/common/expression_functions/arguments.ts
index bfe79e46c6226e..1d2e19436887bc 100644
--- a/src/plugins/expressions/common/expression_functions/arguments.ts
+++ b/src/plugins/expressions/common/expression_functions/arguments.ts
@@ -54,6 +54,10 @@ type UnresolvedArrayTypeToArgumentString =
interface BaseArgumentType {
/** Alternate names for the Function valid for use in the Expression Editor */
aliases?: string[];
+ /**
+ * The flag to mark the function parameter as deprecated.
+ */
+ deprecated?: boolean;
/** Help text for the Argument to be displayed in the Expression Editor */
help: string;
/** Default options for the Argument */
diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts
index 287b126cafbd85..7ce51e3a7d36d9 100644
--- a/src/plugins/expressions/common/expression_functions/expression_function.ts
+++ b/src/plugins/expressions/common/expression_functions/expression_function.ts
@@ -63,6 +63,12 @@ export class ExpressionFunction implements PersistableState
@@ -88,6 +94,7 @@ export class ExpressionFunction implements PersistableState c);
this.inject = inject || identity;
this.extract = extract || ((s) => ({ state: s, references: [] }));
diff --git a/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts b/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts
index 242af9fb439a51..530d0bc5a9c5c7 100644
--- a/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts
+++ b/src/plugins/expressions/common/expression_functions/expression_function_parameter.ts
@@ -16,6 +16,7 @@ export class ExpressionFunctionParameter {
types: ArgumentType['types'];
default?: ArgumentType['default'];
aliases: string[];
+ deprecated: boolean;
multi: boolean;
resolve: boolean;
/**
@@ -25,7 +26,7 @@ export class ExpressionFunctionParameter {
options: T[];
constructor(name: string, arg: ArgumentType) {
- const { required, help, types, aliases, multi, options, resolve, strict } = arg;
+ const { required, help, types, aliases, deprecated, multi, options, resolve, strict } = arg;
if (name === '_') {
throw Error('Arg names must not be _. Use it in aliases instead.');
@@ -37,6 +38,7 @@ export class ExpressionFunctionParameter {
this.types = types || [];
this.default = arg.default;
this.aliases = aliases || [];
+ this.deprecated = !!deprecated;
this.multi = !!multi;
this.options = options || [];
this.resolve = resolve == null ? true : resolve;
diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts
index 14f7c14a6759d3..018ee9e9fac0ca 100644
--- a/src/plugins/expressions/common/expression_functions/types.ts
+++ b/src/plugins/expressions/common/expression_functions/types.ts
@@ -39,6 +39,11 @@ export interface ExpressionFunctionDefinition<
*/
name: Name;
+ /**
+ * The flag to mark the function as deprecated.
+ */
+ deprecated?: boolean;
+
/**
* if set to true function will be disabled (but its migrate function will still be available)
*/
diff --git a/src/plugins/kibana_react/public/code_editor/editor_theme.ts b/src/plugins/kibana_react/public/code_editor/editor_theme.ts
index 9242b7319e5c94..70d3267338ed93 100644
--- a/src/plugins/kibana_react/public/code_editor/editor_theme.ts
+++ b/src/plugins/kibana_react/public/code_editor/editor_theme.ts
@@ -76,6 +76,8 @@ export function createTheme(
{ token: 'keyword.json', foreground: euiTheme.euiColorPrimary },
{ token: 'keyword.flow', foreground: euiTheme.euiColorWarning },
{ token: 'keyword.flow.scss', foreground: euiTheme.euiColorPrimary },
+ // Monaco editor supports strikethrough font style only starting from 0.32.0.
+ { token: 'keyword.deprecated', foreground: euiTheme.euiColorAccent },
{ token: 'operator.scss', foreground: euiTheme.euiColorDarkShade },
{ token: 'operator.sql', foreground: euiTheme.euiColorMediumShade },
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap
index f6dcf46b27d8c4..c11c1a11e73693 100644
--- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap
+++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap
@@ -27,7 +27,21 @@ exports[`ElasticAgentCard props button 1`] = `
}
href="/app/integrations/browse"
- image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
+ image={
+
+ }
paddingSize="l"
title={
@@ -67,7 +81,21 @@ exports[`ElasticAgentCard props category 1`] = `
}
href="/app/integrations/browse/custom"
- image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
+ image={
+
+ }
paddingSize="l"
title={
@@ -107,7 +135,21 @@ exports[`ElasticAgentCard props href 1`] = `
}
href="#"
- image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
+ image={
+
+ }
paddingSize="l"
title={
@@ -147,7 +189,21 @@ exports[`ElasticAgentCard props recommended 1`] = `
}
href="/app/integrations/browse"
- image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
+ image={
+
+ }
paddingSize="l"
title={
@@ -187,7 +243,21 @@ exports[`ElasticAgentCard renders 1`] = `
}
href="/app/integrations/browse"
- image="/plugins/kibanaReact/assets/elastic_agent_card.svg"
+ image={
+
+ }
paddingSize="l"
title={
diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx
index 2cec3045c31c11..a8701fb7b7a344 100644
--- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx
+++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx
@@ -9,7 +9,7 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { CoreStart } from '@kbn/core/public';
-import { EuiButton, EuiCard, EuiTextColor, EuiScreenReaderOnly } from '@elastic/eui';
+import { EuiButton, EuiCard, EuiTextColor, EuiScreenReaderOnly, EuiImage } from '@elastic/eui';
import { useKibana } from '../../../context';
import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page';
import { RedirectAppLinks } from '../../../app_links';
@@ -35,10 +35,24 @@ export const ElasticAgentCard: FunctionComponent = ({
services: { http, application },
} = useKibana();
const addBasePath = http.basePath.prepend;
- const image = addBasePath(`/plugins/kibanaReact/assets/elastic_agent_card.svg`);
+ const imageUrl = addBasePath(`/plugins/kibanaReact/assets/elastic_agent_card.svg`);
const canAccessFleet = application.capabilities.navLinks.integrations;
const hasCategory = category ? `/${category}` : '';
+ const image = (
+
+ );
+
if (!canAccessFleet) {
return (
= {
help: 'A string of text that contains Markdown. To concatenate, pass the `string` function multiple times.',
types: ['string'],
default: '',
+ deprecated: false,
aliases: ['_', 'expression'],
multi: true,
resolve: false,
@@ -33,6 +34,7 @@ const font: ExpressionFunctionParameter