[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>
This commit is contained in:
Sander Philipse 2025-01-24 16:29:53 +01:00 committed by GitHub
parent 938f471781
commit c6f7416efc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 169 additions and 162 deletions

View file

@ -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.",
}),
},
];

View file

@ -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,

View file

@ -46,7 +46,7 @@ export const CreateIndexPage = () => {
grow={false}
solutionNav={searchNavigation?.useClassicNavigation(history)}
>
<KibanaPageTemplate.Section alignment="center" restrictWidth={false} grow>
<KibanaPageTemplate.Section alignment="top" restrictWidth={false}>
{isInitialLoading && <EuiLoadingLogo />}
{hasIndicesStatusFetchError && <LoadIndicesStatusError error={indicesFetchError} />}
{!isInitialLoading && !hasIndicesStatusFetchError && (

View file

@ -75,7 +75,6 @@ export const AddDocumentsCodeExample = ({
apiKey: apiKey || undefined,
};
}, [indexName, elasticsearchUrl, sampleDocuments, codeSampleMappings, indexHasMappings, apiKey]);
const [panelRef, setPanelRef] = useState<HTMLDivElement | null>(null);
return (
<EuiPanel
@ -83,41 +82,67 @@ export const AddDocumentsCodeExample = ({
hasShadow={false}
paddingSize="m"
data-test-subj="SearchIndicesAddDocumentsCode"
panelRef={setPanelRef}
>
<EuiFlexGroup direction="column">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexGroup
justifyContent={indexHasMappings ? 'flexEnd' : 'spaceBetween'}
alignItems="center"
>
{!indexHasMappings && (
<EuiFlexItem css={{ maxWidth: '300px' }} grow={false}>
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h5>
{i18n.translate('xpack.searchIndices.guideSelectors.selectGuideTitle', {
defaultMessage: 'Select a workflow guide',
})}
</h5>
</EuiTitle>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiFlexGroup justifyContent="center" alignItems="center" gutterSize="s">
<EuiFlexItem css={{ maxWidth: '300px' }} grow={false}>
<LanguageSelector
options={LanguageOptions}
selectedLanguage={selectedLanguage}
onSelectLanguage={onSelectLanguage}
showLabel
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TryInConsoleButton
request={
!indexHasMappings
? `${ingestExamples.sense.updateMappingsCommand(
codeParams
)}\n\n${ingestExamples.sense.ingestCommand(codeParams)}`
: ingestExamples.sense.ingestCommand(codeParams)
}
application={application}
sharePlugin={share}
consolePlugin={consolePlugin}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem>
{!indexHasMappings && (
<EuiFlexItem grow={false}>
<GuideSelector
selectedWorkflowId={selectedWorkflowId}
onChange={(workflowId: WorkflowId) => {
setSelectedWorkflowId(workflowId);
usageTracker.click([
AnalyticsEvents.indexDetailsCodeLanguageSelect,
`${AnalyticsEvents.indexDetailsCodeLanguageSelect}_${workflowId}`,
AnalyticsEvents.indexDetailsWorkflowSelect,
`${AnalyticsEvents.indexDetailsWorkflowSelect}_${workflowId}`,
]);
}}
showTour
container={panelRef}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<TryInConsoleButton
request={
!indexHasMappings
? `${ingestExamples.sense.updateMappingsCommand(
codeParams
)}\n\n${ingestExamples.sense.ingestCommand(codeParams)}`
: ingestExamples.sense.ingestCommand(codeParams)
}
application={application}
sharePlugin={share}
consolePlugin={consolePlugin}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{!!workflow && (
<EuiFlexItem>
<EuiTitle>
@ -129,14 +154,6 @@ export const AddDocumentsCodeExample = ({
</EuiText>
</EuiFlexItem>
)}
<EuiFlexItem css={{ maxWidth: '300px' }} grow={false}>
<LanguageSelector
options={LanguageOptions}
selectedLanguage={selectedLanguage}
onSelectLanguage={onSelectLanguage}
showLabel
/>
</EuiFlexItem>
{selectedCodeExamples.installCommand && (
<EuiFlexItem>
<CodeSample

View file

@ -39,8 +39,8 @@ export const APIKeyCallout = ({ apiKey }: APIKeyCalloutProps) => {
<EuiPanel
paddingSize="m"
hasShadow={false}
hasBorder={true}
color="plain"
hasBorder={false}
color="subdued"
data-test-subj={dataTestSubj}
>
<EuiFlexGroup direction="column" gutterSize="s">

View file

@ -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 = ({
<APIKeyCallout apiKey={apiKey} />
</EuiFlexItem>
)}
<EuiHorizontalRule margin="none" />
<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexStart">
<EuiFlexItem grow={false}>
<GuideSelector
selectedWorkflowId={selectedWorkflow?.id || 'default'}
onChange={changeWorkflowId}
showTour={false}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TryInConsoleButton
request={selectedCodeExamples.sense.createIndex(codeParams)}
application={application}
sharePlugin={share}
consolePlugin={consolePlugin}
telemetryId={`${selectedLanguage}_create_index`}
onClick={() => {
usageTracker.click([
analyticsEvents.runInConsole,
`${analyticsEvents.runInConsole}_${selectedLanguage}`,
]);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiTitle size="xs">
<h5>
{i18n.translate('xpack.searchIndices.guideSelectors.selectGuideTitle', {
defaultMessage: 'Select a workflow guide',
})}
</h5>
</EuiTitle>
<EuiSpacer />
<GuideSelector
selectedWorkflowId={selectedWorkflow?.id || 'default'}
onChange={changeWorkflowId}
showTour={false}
/>
</EuiFlexItem>
{!!selectedWorkflow && (
<>
@ -109,13 +108,30 @@ export const CreateIndexCodeView = ({
</EuiFlexItem>
</>
)}
<EuiFlexItem css={{ maxWidth: '300px' }}>
<LanguageSelector
options={LanguageOptions}
selectedLanguage={selectedLanguage}
onSelectLanguage={changeCodingLanguage}
/>
</EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false} css={{ maxWidth: '300px' }}>
<LanguageSelector
options={LanguageOptions}
selectedLanguage={selectedLanguage}
onSelectLanguage={changeCodingLanguage}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TryInConsoleButton
request={selectedCodeExamples.sense.createIndex(codeParams)}
application={application}
sharePlugin={share}
consolePlugin={consolePlugin}
telemetryId={`${selectedLanguage}_create_index`}
onClick={() => {
usageTracker.click([
analyticsEvents.runInConsole,
`${analyticsEvents.runInConsole}_${selectedLanguage}`,
]);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
{selectedCodeExample.installCommand && (
<CodeSample
title={selectedCodeExamples.installTitle}

View file

@ -26,7 +26,7 @@ import { docLinks } from '../../../common/doc_links';
import { useKibana } from '../../hooks/use_kibana';
import { CreateIndexViewMode } from '../../types';
const MAX_WIDTH = '650px';
const WIDTH = '980px';
export interface CreateIndexPanelProps {
children: React.ReactNode | React.ReactNode[];
@ -65,7 +65,7 @@ export const CreateIndexPanel = ({
hasShadow={false}
hasBorder
style={{
maxWidth: MAX_WIDTH,
width: WIDTH,
margin: '0 auto',
padding: euiTheme.size.l,
paddingTop: euiTheme.size.m,

View file

@ -5,44 +5,13 @@
* 2.0.
*/
import React, { useState, useEffect } from 'react';
import React from 'react';
import {
EuiButton,
EuiCard,
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
EuiTourStep,
} from '@elastic/eui';
import { EuiCard, EuiText, EuiFlexGroup, EuiFlexItem, EuiTourStep } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Workflow, WorkflowId, workflows } from '../../code_examples/workflows';
import { WorkflowId, workflows } from '../../code_examples/workflows';
import { useGuideTour } from './hooks/use_guide_tour';
interface PopoverCardProps {
workflow: Workflow;
isSelected: boolean;
onChange: (workflowId: WorkflowId) => void;
}
const PopoverCard: React.FC<PopoverCardProps> = ({ workflow, onChange, isSelected }) => (
<EuiCard
title={workflow.title}
hasBorder
titleSize="xs"
description={
<EuiText color="subdued" size="s">
{workflow.summary}
</EuiText>
}
selectable={{
onClick: () => onChange(workflow.id),
isSelected,
}}
/>
);
interface GuideSelectorProps {
selectedWorkflowId: WorkflowId;
onChange: (workflow: WorkflowId) => void;
@ -54,60 +23,9 @@ export const GuideSelector: React.FC<GuideSelectorProps> = ({
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 = (
<EuiButton
color="text"
iconType="arrowDown"
iconSide="right"
onClick={onPopoverClick}
data-test-subj="workflowSelectorButton"
>
{i18n.translate('xpack.searchIndices.guideSelector.selectWorkflow', {
defaultMessage: 'Select a guide',
})}
</EuiButton>
);
const Popover = () => (
<EuiPopover
anchorPosition="downRight"
button={PopoverButton}
isOpen={isPopoverOpen}
closePopover={closePopover}
title="Select a workflow"
container={container || undefined}
>
<>
<EuiFlexGroup gutterSize="m" style={{ maxWidth: '960px' }}>
{workflows.map((workflow) => (
<EuiFlexItem key={workflow.id}>
<PopoverCard
workflow={workflow}
isSelected={workflow.id === selectedWorkflowId}
onChange={(value) => onChange(value)}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
</>
</EuiPopover>
);
return showTour ? (
<EuiTourStep
content={
@ -124,14 +42,45 @@ export const GuideSelector: React.FC<GuideSelectorProps> = ({
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"
>
<Popover />
<GuideSelectorTiles selectedWorkflowId={selectedWorkflowId} onChange={onChange} />
</EuiTourStep>
) : (
<Popover />
<GuideSelectorTiles selectedWorkflowId={selectedWorkflowId} onChange={onChange} />
);
};
const GuideSelectorTiles: React.FC<Pick<GuideSelectorProps, 'selectedWorkflowId' | 'onChange'>> = ({
selectedWorkflowId,
onChange,
}) => {
return (
<EuiFlexGroup gutterSize="m">
{workflows.map((workflow) => {
const isSelected = selectedWorkflowId === workflow.id;
return (
<EuiFlexItem key={workflow.id}>
<EuiCard
title={workflow.title}
hasBorder={!isSelected}
titleSize="xs"
description={
<EuiText color="subdued" size="s">
{workflow.summary}
</EuiText>
}
selectable={{
onClick: () => onChange(workflow.id),
isSelected,
}}
/>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
);
};

View file

@ -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<WorkflowId>('default');
const localStorageWorkflow = localStorage.getItem(WORKFLOW_LOCALSTORAGE_KEY);
const [selectedWorkflowId, setSelectedWorkflowId] = useState<WorkflowId>(
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),

View file

@ -50,7 +50,11 @@ export const ElasticsearchStart: React.FC<ElasticsearchStartProps> = () => {
: 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<ElasticsearchStartProps> = () => {
installCommands: AnalyticsEvents.startCreateIndexCodeCopyInstall,
createIndex: AnalyticsEvents.startCreateIndexCodeCopy,
}}
selectedCodeExamples={selectedCodeExamples}
/>
)}
</CreateIndexPanel>

View file

@ -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() {

View file

@ -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() {