From c6f7416efcf50ddc526fda46c893579918b48029 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Fri, 24 Jan 2025 16:29:53 +0100 Subject: [PATCH] [ES3] Fix onboarding tiles to new design (#208069) ## Summary This adapts the onboarding guide for Elasticsearch to: - use in-page tiles - remember its guide selection in local storage - actually switch the code correctly on the start page https://github.com/user-attachments/assets/9a23b7a6-828a-4d37-a460-975dd526eafe UPDATED SCREEN RECORDING: https://github.com/user-attachments/assets/7526f1d5-b85c-4096-85c5-9cf35b1bd757 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/code_examples/workflows.ts | 2 +- .../components/create_index/create_index.tsx | 7 +- .../create_index/create_index_page.tsx | 2 +- .../add_documents_code_example.tsx | 77 ++++++----- .../components/shared/api_key_callout.tsx | 4 +- .../shared/create_index_code_view.tsx | 84 +++++++----- .../components/shared/create_index_panel.tsx | 4 +- .../components/shared/guide_selector.tsx | 125 ++++++------------ .../components/shared/hooks/use_workflow.tsx | 16 ++- .../components/start/elasticsearch_start.tsx | 7 +- .../functional/page_objects/search_start.ts | 2 + .../svl_search_elasticsearch_start_page.ts | 1 + 12 files changed, 169 insertions(+), 162 deletions(-) diff --git a/x-pack/solutions/search/plugins/search_indices/public/code_examples/workflows.ts b/x-pack/solutions/search/plugins/search_indices/public/code_examples/workflows.ts index 29d2a64254028..d7e8c42139d32 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/code_examples/workflows.ts +++ b/x-pack/solutions/search/plugins/search_indices/public/code_examples/workflows.ts @@ -41,7 +41,7 @@ export const workflows: Workflow[] = [ id: 'semantic', summary: i18n.translate('xpack.searchIndices.workflows.semanticSummary', { defaultMessage: - "Semantic search in Elasticsearch is now simpler with the new semantic_text field type. This example walks through setting up your index with a semantic_text field, which uses Elastic's built-in ELSER machine learning model. If the model is not running, a new deployment will start once the mappings are defined.", + "Use a semantic_text field type and Elastic's built-in ELSER machine learning model.", }), }, ]; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/create_index/create_index.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/create_index/create_index.tsx index 2f4081b99d486..2266cc4b6c5bc 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/create_index/create_index.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/create_index/create_index.tsx @@ -52,7 +52,11 @@ export const CreateIndex = ({ indicesData }: CreateIndexProps) => { ? CreateIndexViewMode.Code : CreateIndexViewMode.UI ); - const { workflow, setSelectedWorkflowId } = useWorkflow(); + const { + workflow, + setSelectedWorkflowId, + createIndexExamples: selectedCodeExamples, + } = useWorkflow(); const usageTracker = useUsageTracker(); const onChangeView = useCallback( (id: string) => { @@ -113,6 +117,7 @@ export const CreateIndex = ({ indicesData }: CreateIndexProps) => { ]); }} selectedWorkflow={workflow} + selectedCodeExamples={selectedCodeExamples} canCreateApiKey={userPrivileges?.privileges.canCreateApiKeys} analyticsEvents={{ runInConsole: AnalyticsEvents.createIndexRunInConsole, diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/create_index/create_index_page.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/create_index/create_index_page.tsx index 1466fbdcec6a6..904ecdb471c11 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/create_index/create_index_page.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/create_index/create_index_page.tsx @@ -46,7 +46,7 @@ export const CreateIndexPage = () => { grow={false} solutionNav={searchNavigation?.useClassicNavigation(history)} > - + {isInitialLoading && } {hasIndicesStatusFetchError && } {!isInitialLoading && !hasIndicesStatusFetchError && ( diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/index_documents/add_documents_code_example.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/index_documents/add_documents_code_example.tsx index e4655f6a858b5..c2bf886036fb2 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/index_documents/add_documents_code_example.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/index_documents/add_documents_code_example.tsx @@ -75,7 +75,6 @@ export const AddDocumentsCodeExample = ({ apiKey: apiKey || undefined, }; }, [indexName, elasticsearchUrl, sampleDocuments, codeSampleMappings, indexHasMappings, apiKey]); - const [panelRef, setPanelRef] = useState(null); return ( - + {!indexHasMappings && ( - + + +
+ {i18n.translate('xpack.searchIndices.guideSelectors.selectGuideTitle', { + defaultMessage: 'Select a workflow guide', + })} +
+
+
+ )} + + + + + + + + + + +
+ + {!indexHasMappings && ( + { setSelectedWorkflowId(workflowId); usageTracker.click([ - AnalyticsEvents.indexDetailsCodeLanguageSelect, - `${AnalyticsEvents.indexDetailsCodeLanguageSelect}_${workflowId}`, + AnalyticsEvents.indexDetailsWorkflowSelect, + `${AnalyticsEvents.indexDetailsWorkflowSelect}_${workflowId}`, ]); }} showTour - container={panelRef} /> )} - - - -
+ {!!workflow && ( @@ -129,14 +154,6 @@ export const AddDocumentsCodeExample = ({ )} - - - {selectedCodeExamples.installCommand && ( { diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/shared/create_index_code_view.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/shared/create_index_code_view.tsx index 389e5dcd0b989..fe717686399de 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/shared/create_index_code_view.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/shared/create_index_code_view.tsx @@ -5,10 +5,18 @@ * 2.0. */ import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; import { TryInConsoleButton } from '@kbn/try-in-console'; import { useSearchApiKey } from '@kbn/search-api-keys-components'; +import { i18n } from '@kbn/i18n'; import { Languages, AvailableLanguages, LanguageOptions } from '../../code_examples'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; @@ -17,10 +25,10 @@ import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url'; import { APIKeyCallout } from './api_key_callout'; import { CodeSample } from './code_sample'; -import { useWorkflow } from './hooks/use_workflow'; import { LanguageSelector } from './language_selector'; import { GuideSelector } from './guide_selector'; import { Workflow, WorkflowId } from '../../code_examples/workflows'; +import { CreateIndexCodeExamples } from '../../types'; export interface CreateIndexCodeViewProps { selectedLanguage: AvailableLanguages; @@ -34,6 +42,7 @@ export interface CreateIndexCodeViewProps { installCommands: string; createIndex: string; }; + selectedCodeExamples: CreateIndexCodeExamples; } export const CreateIndexCodeView = ({ @@ -44,10 +53,10 @@ export const CreateIndexCodeView = ({ selectedWorkflow, indexName, selectedLanguage, + selectedCodeExamples, }: CreateIndexCodeViewProps) => { const { application, share, console: consolePlugin } = useKibana().services; const usageTracker = useUsageTracker(); - const { createIndexExamples: selectedCodeExamples } = useWorkflow(); const elasticsearchUrl = useElasticsearchUrl(); const { apiKey } = useSearchApiKey(); @@ -70,31 +79,21 @@ export const CreateIndexCodeView = ({ )} + - - - - - - { - usageTracker.click([ - analyticsEvents.runInConsole, - `${analyticsEvents.runInConsole}_${selectedLanguage}`, - ]); - }} - /> - - + +
+ {i18n.translate('xpack.searchIndices.guideSelectors.selectGuideTitle', { + defaultMessage: 'Select a workflow guide', + })} +
+
+ +
{!!selectedWorkflow && ( <> @@ -109,13 +108,30 @@ export const CreateIndexCodeView = ({ )} - - - + + + + + + { + usageTracker.click([ + analyticsEvents.runInConsole, + `${analyticsEvents.runInConsole}_${selectedLanguage}`, + ]); + }} + /> + + {selectedCodeExample.installCommand && ( void; -} - -const PopoverCard: React.FC = ({ workflow, onChange, isSelected }) => ( - - {workflow.summary} - - } - selectable={{ - onClick: () => onChange(workflow.id), - isSelected, - }} - /> -); - interface GuideSelectorProps { selectedWorkflowId: WorkflowId; onChange: (workflow: WorkflowId) => void; @@ -54,60 +23,9 @@ export const GuideSelector: React.FC = ({ selectedWorkflowId, onChange, showTour, - container, }) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); const { tourIsOpen, setTourIsOpen } = useGuideTour(); - const onPopoverClick = () => { - setIsPopoverOpen(() => !isPopoverOpen); - }; - - useEffect(() => { - closePopover(); - }, [selectedWorkflowId]); - - const closePopover = () => setIsPopoverOpen(false); - - const PopoverButton = ( - - {i18n.translate('xpack.searchIndices.guideSelector.selectWorkflow', { - defaultMessage: 'Select a guide', - })} - - ); - - const Popover = () => ( - - <> - - {workflows.map((workflow) => ( - - onChange(value)} - /> - - ))} - - - - ); - return showTour ? ( = ({ onFinish={() => setTourIsOpen(false)} step={1} stepsTotal={1} - title={i18n.translate('xpack.searchIndices.touTitle', { + title={i18n.translate('xpack.searchIndices.tourTitle', { defaultMessage: 'New guides available!', })} anchorPosition="rightUp" > - + ) : ( - + + ); +}; + +const GuideSelectorTiles: React.FC> = ({ + selectedWorkflowId, + onChange, +}) => { + return ( + + {workflows.map((workflow) => { + const isSelected = selectedWorkflowId === workflow.id; + return ( + + + {workflow.summary} + + } + selectable={{ + onClick: () => onChange(workflow.id), + isSelected, + }} + /> + + ); + })} + ); }; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/shared/hooks/use_workflow.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/shared/hooks/use_workflow.tsx index 0db49738aed8d..e04a00c6f289d 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/shared/hooks/use_workflow.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/shared/hooks/use_workflow.tsx @@ -40,13 +40,25 @@ const workflowIdToIngestDataExamples = (type: WorkflowId) => { } }; +const WORKFLOW_LOCALSTORAGE_KEY = 'search_onboarding_workflow'; + +function isWorkflowId(value: string | null): value is WorkflowId { + return value === 'default' || value === 'vector' || value === 'semantic'; +} + export const useWorkflow = () => { // TODO: in the future this will be dynamic based on the onboarding token // or project sub-type - const [selectedWorkflowId, setSelectedWorkflowId] = useState('default'); + const localStorageWorkflow = localStorage.getItem(WORKFLOW_LOCALSTORAGE_KEY); + const [selectedWorkflowId, setSelectedWorkflowId] = useState( + isWorkflowId(localStorageWorkflow) ? localStorageWorkflow : 'default' + ); return { selectedWorkflowId, - setSelectedWorkflowId, + setSelectedWorkflowId: (workflowId: WorkflowId) => { + localStorage.setItem(WORKFLOW_LOCALSTORAGE_KEY, workflowId); + setSelectedWorkflowId(workflowId); + }, workflow: workflows.find((workflow) => workflow.id === selectedWorkflowId), createIndexExamples: workflowIdToCreateIndexExamples(selectedWorkflowId), ingestExamples: workflowIdToIngestDataExamples(selectedWorkflowId), diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/start/elasticsearch_start.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/start/elasticsearch_start.tsx index 0b359688353a5..9f237e482a9ae 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/start/elasticsearch_start.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/start/elasticsearch_start.tsx @@ -50,7 +50,11 @@ export const ElasticsearchStart: React.FC = () => { : CreateIndexViewMode.UI ); const usageTracker = useUsageTracker(); - const { workflow, setSelectedWorkflowId } = useWorkflow(); + const { + workflow, + setSelectedWorkflowId, + createIndexExamples: selectedCodeExamples, + } = useWorkflow(); useEffect(() => { usageTracker.load(AnalyticsEvents.startPageOpened); @@ -131,6 +135,7 @@ export const ElasticsearchStart: React.FC = () => { installCommands: AnalyticsEvents.startCreateIndexCodeCopyInstall, createIndex: AnalyticsEvents.startCreateIndexCodeCopy, }} + selectedCodeExamples={selectedCodeExamples} /> )} diff --git a/x-pack/test/functional/page_objects/search_start.ts b/x-pack/test/functional/page_objects/search_start.ts index 7d73c17a175a6..31a588a8dc1c5 100644 --- a/x-pack/test/functional/page_objects/search_start.ts +++ b/x-pack/test/functional/page_objects/search_start.ts @@ -54,6 +54,8 @@ export function SearchStartProvider({ getService }: FtrProviderContext) { }, async clickSkipButton() { await testSubjects.existOrFail('createIndexSkipBtn'); + const element = await testSubjects.find('createIndexSkipBtn'); + await element.scrollIntoView(); await testSubjects.click('createIndexSkipBtn'); }, async expectCreateIndexButtonToExist() { diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts index aadb41d6f432a..c0fcf55a97abb 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_elasticsearch_start_page.ts @@ -54,6 +54,7 @@ export function SvlSearchElasticsearchStartPageProvider({ getService }: FtrProvi }, async clickSkipButton() { await testSubjects.existOrFail('createIndexSkipBtn'); + await testSubjects.scrollIntoView('createIndexSkipBtn'); await testSubjects.click('createIndexSkipBtn'); }, async expectCreateIndexButtonToExist() {