diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 173264aee731e..2ea0215cad04d 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 5bf5107dc0349..44dcb0901dd7c 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 5a9c6a68d6bb3..2fe073946627a 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 7b715bb56a74c..1a84516fbdd8d 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/assets/discover_tour/add_fields.gif b/src/plugins/discover/public/assets/discover_tour/add_fields.gif new file mode 100644 index 0000000000000..c955a9aa99e08 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 0000000000000..7131fed839478 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 0000000000000..d3aeedb513c1e 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 0000000000000..66033d03d8fd2 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 0000000000000..6d22b947a206f 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_expand_button.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx index 6765a8d24f91a..a64d8521f503e 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 ( { + 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 0000000000000..030876291aeea --- /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 0000000000000..94bbfaf6555d2 --- /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 0000000000000..610fc0907ea5a --- /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 0000000000000..b60c79a80f54d --- /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/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 8330751cf13f0..b4a33119c23b2 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/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 262008e1bc784..9f8fb342189f0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2901,9 +2901,6 @@ "discover.docExplorerCallout.learnMore": "詳細", "discover.docExplorerCallout.tryDocumentExplorer": "ドキュメントエクスプローラーを試す", "discover.docExplorerUpdateCallout.closeButtonAriaLabel": "閉じる", - "discover.docExplorerUpdateCallout.documentExplorer": "ドキュメントエクスプローラー", - "discover.docExplorerUpdateCallout.fieldStatistics": "フィールド統計情報", - "discover.docExplorerUpdateCallout.headerMessage": "より効率的な探索方法", "discover.docTable.documentsNavigation": "ドキュメントナビゲーション", "discover.docTable.limitedSearchResultLabel": "{resultCount}件の結果のみが表示されます。検索結果を絞り込みます。", "discover.docTable.noResultsTitle": "結果が見つかりませんでした", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f3e59c51e5693..11de6fba16ad0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2909,11 +2909,7 @@ "discover.docExplorerCallout.headerMessage": "更好的浏览方式", "discover.docExplorerCallout.learnMore": "了解详情", "discover.docExplorerCallout.tryDocumentExplorer": "试用 Document Explorer", - "discover.docExplorerUpdateCallout.bodyMessage": "体验新的 {documentExplorer}。通过 {fieldStatistics} 了解您数据的形状。", "discover.docExplorerUpdateCallout.closeButtonAriaLabel": "关闭", - "discover.docExplorerUpdateCallout.documentExplorer": "Document Explorer", - "discover.docExplorerUpdateCallout.fieldStatistics": "字段统计信息", - "discover.docExplorerUpdateCallout.headerMessage": "更好的浏览方式", "discover.docTable.documentsNavigation": "文档导航", "discover.docTable.limitedSearchResultLabel": "仅限于 {resultCount} 个结果。优化您的搜索。", "discover.docTable.noResultsTitle": "找不到结果",