Search indices consume onboarding token (#211755)

## Summary

This consumes the onboarding token propagated by Cloud to determine
which workflow to show in Serverless and ECH.

Best way to test this locally when running on localhost:5601:

In Serverless:
- Go to
http://localhost:5601/app/cloud/onboarding?next=/app/elasticsearch&onboarding_token=vector
- You should be redirected to the getting started flow
- Switch to code view and you should have vector search selected
- Go to
http://localhost:5601/app/cloud/onboarding?next=/app/enterprise_search/overview&onboarding_token=vectorsearch
- You should be redirected to the getting started flow
- Switch to code view and now you should have vector search selected

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Sander Philipse 2025-02-21 17:16:08 +01:00 committed by GitHub
parent cb71dff86e
commit 6507cc3fd0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 156 additions and 21 deletions

View file

@ -13,3 +13,5 @@ export const plugin = async (initializerContext: PluginInitializerContext) => {
const { CloudPlugin } = await import('./plugin');
return new CloudPlugin(initializerContext);
};
export { getOnboardingToken } from './saved_objects';

View file

@ -253,7 +253,7 @@ export class CloudPlugin implements Plugin<CloudSetup, CloudStart> {
const nextCandidateRoute = parseNextURL(request.url.href);
const route = nextCandidateRoute === '/' ? defaultRoute : nextCandidateRoute;
// need to get reed of ../../ to make sure we will not be out of space basePath
// need to get rid of ../../ to make sure we will not be out of space basePath
const normalizedRoute = new URL(route, 'https://localhost');
const queryOnboardingToken = request.query?.onboarding_token ?? undefined;
@ -265,6 +265,7 @@ export class CloudPlugin implements Plugin<CloudSetup, CloudStart> {
: undefined;
const solutionType = this.config.onboarding?.default_solution;
if (queryOnboardingToken || queryOnboardingSecurity) {
core
.getStartServices()

View file

@ -5,7 +5,9 @@
* 2.0.
*/
import { Logger, SavedObjectsServiceSetup } from '@kbn/core/server';
import { Logger, SavedObjectsClientContract, SavedObjectsServiceSetup } from '@kbn/core/server';
import { CloudDataAttributes } from '../../common/types';
import { CLOUD_DATA_SAVED_OBJECT_ID } from '../routes/constants';
export const CLOUD_DATA_SAVED_OBJECT_TYPE = 'cloud' as const;
@ -25,3 +27,19 @@ export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup, logger
modelVersions: {},
});
}
// needs a client with permissions to read the cloud data saved object
export async function getOnboardingToken(
savedObjectsClient: SavedObjectsClientContract
): Promise<string | null> {
let cloudDataSo = null;
try {
cloudDataSo = await savedObjectsClient.get<CloudDataAttributes>(
CLOUD_DATA_SAVED_OBJECT_TYPE,
CLOUD_DATA_SAVED_OBJECT_ID
);
} catch (error) {
cloudDataSo = null;
}
return cloudDataSo?.attributes.onboardingData?.token || null;
}

View file

@ -9,3 +9,5 @@ export * from './src/connector_icon';
export * from './src/decorative_horizontal_stepper';
export * from './src/form_info_field/form_info_field';
export * from './src/search_empty_prompt';
export * from './src/constants';
export * from './src/types';

View file

@ -0,0 +1,8 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const WORKFLOW_LOCALSTORAGE_KEY = 'search_onboarding_workflow';

View file

@ -0,0 +1,8 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type WorkflowId = 'default' | 'vector' | 'semantic';

View file

@ -13,3 +13,5 @@ export const POST_CREATE_INDEX_ROUTE = '/internal/search_indices/indices/create'
export const INDEX_DOCUMENT_ROUTE = '/internal/search_indices/{indexName}/documents/{id}';
export const SEARCH_DOCUMENTS_ROUTE = '/internal/search_indices/{indexName}/documents/search';
export const GET_ONBOARDING_TOKEN_ROUTE = '/internal/search_indices/onboarding_token';

View file

@ -9,6 +9,10 @@ export interface IndicesStatusResponse {
indexNames: string[];
}
export interface OnboardingTokenResponse {
token: string | null;
}
export interface UserStartPrivilegesResponse {
privileges: {
canCreateApiKeys: boolean;

View file

@ -6,8 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
export type WorkflowId = 'default' | 'vector' | 'semantic';
import { WorkflowId } from '@kbn/search-shared-ui';
export interface Workflow {
title: string;

View file

@ -7,6 +7,7 @@
import React, { useCallback, useState } from 'react';
import { WorkflowId } from '@kbn/search-shared-ui';
import type { IndicesStatusResponse } from '../../../common';
import { AnalyticsEvents } from '../../analytics/constants';
@ -22,7 +23,6 @@ import { CreateIndexPanel } from '../shared/create_index_panel/create_index_pane
import { CreateIndexCodeView } from './create_index_code_view';
import { CreateIndexUIView } from './create_index_ui_view';
import { WorkflowId } from '../../code_examples/workflows';
import { useWorkflow } from '../shared/hooks/use_workflow';
function initCreateIndexState() {

View file

@ -27,6 +27,10 @@ jest.mock('../../hooks/use_elasticsearch_url', () => ({
useElasticsearchUrl: jest.fn(),
}));
jest.mock('../../hooks/api/use_onboarding_data', () => ({
useOnboardingTokenQuery: jest.fn().mockReturnValue({ data: { token: 'default' } }),
}));
jest.mock('@kbn/search-api-keys-components', () => ({
useSearchApiKey: jest.fn().mockReturnValue({ apiKey: 'test-api-key' }),
}));

View file

@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n';
import { TryInConsoleButton } from '@kbn/try-in-console';
import { useSearchApiKey } from '@kbn/search-api-keys-components';
import { WorkflowId } from '@kbn/search-shared-ui';
import { useKibana } from '../../hooks/use_kibana';
import { IngestCodeSnippetParameters } from '../../types';
import { LanguageSelector } from '../shared/language_selector';
@ -24,7 +25,6 @@ import { generateSampleDocument } from '../../utils/document_generation';
import { getDefaultCodingLanguage } from '../../utils/language';
import { GuideSelector } from '../shared/guide_selector';
import { useWorkflow } from '../shared/hooks/use_workflow';
import { WorkflowId } from '../../code_examples/workflows';
export const exampleTexts = [
'Yellowstone National Park is one of the largest national parks in the United States. It ranges from the Wyoming to Montana and Idaho, and contains an area of 2,219,791 acress across three different states. Its most famous for hosting the geyser Old Faithful and is centered on the Yellowstone Caldera, the largest super volcano on the American continent. Yellowstone is host to hundreds of species of animal, many of which are endangered or threatened. Most notably, it contains free-ranging herds of bison and elk, alongside bears, cougars and wolves. The national park receives over 4.5 million visitors annually and is a UNESCO World Heritage Site.',

View file

@ -17,6 +17,7 @@ import { TryInConsoleButton } from '@kbn/try-in-console';
import { useSearchApiKey } from '@kbn/search-api-keys-components';
import { i18n } from '@kbn/i18n';
import { WorkflowId } from '@kbn/search-shared-ui';
import { Languages, AvailableLanguages, LanguageOptions } from '../../code_examples';
import { useUsageTracker } from '../../hooks/use_usage_tracker';
@ -27,7 +28,7 @@ import { APIKeyCallout } from './api_key_callout';
import { CodeSample } from './code_sample';
import { LanguageSelector } from './language_selector';
import { GuideSelector } from './guide_selector';
import { Workflow, WorkflowId } from '../../code_examples/workflows';
import { Workflow } from '../../code_examples/workflows';
import { CreateIndexCodeExamples } from '../../types';
export interface CreateIndexCodeViewProps {

View file

@ -9,7 +9,8 @@ import React from 'react';
import { EuiCard, EuiText, EuiFlexGroup, EuiFlexItem, EuiTourStep } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { WorkflowId, workflows } from '../../code_examples/workflows';
import { WorkflowId } from '@kbn/search-shared-ui';
import { workflows } from '../../code_examples/workflows';
import { useGuideTour } from './hooks/use_guide_tour';
interface GuideSelectorProps {

View file

@ -5,18 +5,20 @@
* 2.0.
*/
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { WORKFLOW_LOCALSTORAGE_KEY, WorkflowId } from '@kbn/search-shared-ui';
import {
DenseVectorIngestDataCodeExamples,
SemanticIngestDataCodeExamples,
DefaultIngestDataCodeExamples,
} from '../../../code_examples/ingest_data';
import { WorkflowId, workflows } from '../../../code_examples/workflows';
import { workflows } from '../../../code_examples/workflows';
import {
DefaultCodeExamples,
DenseVectorCodeExamples,
SemanticCodeExamples,
} from '../../../code_examples/create_index';
import { useOnboardingTokenQuery } from '../../../hooks/api/use_onboarding_data';
const workflowIdToCreateIndexExamples = (type: WorkflowId) => {
switch (type) {
@ -40,24 +42,43 @@ 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';
}
// possible onboarding tokens now: 'general' | 'vector' | 'timeseries' | 'semantic' for serverless, 'vectorsearch' or 'search' for hosted
// note: test with http://localhost:5601/app/cloud/onboarding?next=/app/elasticsearch&onboarding_token=vector in Serverless
// http://localhost:5601/app/cloud/onboarding?next=/app/enterprise_search/overview&onboarding_token=vector in Hosted
function onboardingTokenToWorkflowId(token: string | undefined | null): WorkflowId {
switch (token) {
case 'vector':
return 'vector';
case 'vectorsearch':
return 'vector';
case 'semantic':
return 'semantic';
default:
return 'default';
}
}
export const useWorkflow = () => {
// TODO: in the future this will be dynamic based on the onboarding token
// or project sub-type
const localStorageWorkflow = localStorage.getItem(WORKFLOW_LOCALSTORAGE_KEY);
const [selectedWorkflowId, setSelectedWorkflowId] = useState<WorkflowId>(
isWorkflowId(localStorageWorkflow) ? localStorageWorkflow : 'default'
);
const workflowId = isWorkflowId(localStorageWorkflow) ? localStorageWorkflow : null;
const [selectedWorkflowId, setSelectedWorkflowId] = useState<WorkflowId>(workflowId || 'default');
const { data } = useOnboardingTokenQuery();
useEffect(() => {
if (data?.token && !localStorageWorkflow) {
setSelectedWorkflowId(onboardingTokenToWorkflowId(data.token));
}
}, [data, localStorageWorkflow]);
return {
selectedWorkflowId,
setSelectedWorkflowId: (workflowId: WorkflowId) => {
localStorage.setItem(WORKFLOW_LOCALSTORAGE_KEY, workflowId);
setSelectedWorkflowId(workflowId);
setSelectedWorkflowId: (newWorkflowId: WorkflowId) => {
localStorage.setItem(WORKFLOW_LOCALSTORAGE_KEY, newWorkflowId);
setSelectedWorkflowId(newWorkflowId);
},
workflow: workflows.find((workflow) => workflow.id === selectedWorkflowId),
createIndexExamples: workflowIdToCreateIndexExamples(selectedWorkflowId),

View file

@ -8,6 +8,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { WorkflowId } from '@kbn/search-shared-ui';
import type { IndicesStatusResponse } from '../../../common';
import { AnalyticsEvents } from '../../analytics/constants';
@ -23,7 +24,6 @@ import { CreateIndexFormState, CreateIndexViewMode } from '../../types';
import { CreateIndexPanel } from '../shared/create_index_panel/create_index_panel';
import { useKibana } from '../../hooks/use_kibana';
import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions';
import { WorkflowId } from '../../code_examples/workflows';
import { useWorkflow } from '../shared/hooks/use_workflow';
function initCreateIndexState(): CreateIndexFormState {

View file

@ -8,6 +8,7 @@
export enum QueryKeys {
FetchIndex = 'fetchIndex',
FetchMapping = 'fetchMapping',
FetchOnboardingToken = 'fetchOnboardingToken',
FetchSearchIndicesStatus = 'fetchSearchIndicesStatus',
FetchUserStartPrivileges = 'fetchUserStartPrivileges',
SearchDocuments = 'searchDocuments',

View file

@ -0,0 +1,26 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useQuery } from '@tanstack/react-query';
import type { UseQueryResult } from '@tanstack/react-query';
import { GET_ONBOARDING_TOKEN_ROUTE } from '../../../common/routes';
import type { OnboardingTokenResponse } from '../../../common/types';
import { QueryKeys } from '../../constants';
import { useKibana } from '../use_kibana';
export const useOnboardingTokenQuery = (): UseQueryResult<OnboardingTokenResponse> => {
const { http } = useKibana().services;
return useQuery({
refetchInterval: false,
retry: true,
queryKey: [QueryKeys.FetchOnboardingToken],
queryFn: () => http.get<OnboardingTokenResponse>(GET_ONBOARDING_TOKEN_ROUTE),
});
};

View file

@ -12,9 +12,11 @@ import { registerSearchApiKeysRoutes } from '@kbn/search-api-keys-server';
import { registerIndicesRoutes } from './indices';
import { registerStatusRoutes } from './status';
import { registerDocumentRoutes } from './documents';
import { registerOnboardingRoutes } from './onboarding';
export function defineRoutes(router: IRouter, logger: Logger) {
registerIndicesRoutes(router, logger);
registerOnboardingRoutes(router, logger);
registerStatusRoutes(router, logger);
registerSearchApiKeysRoutes(router, logger);
registerDocumentRoutes(router, logger);

View file

@ -0,0 +1,35 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { IRouter } from '@kbn/core/server';
import type { Logger } from '@kbn/logging';
import { getOnboardingToken } from '@kbn/cloud-plugin/server';
import { GET_ONBOARDING_TOKEN_ROUTE } from '../../common/routes';
export function registerOnboardingRoutes(router: IRouter, logger: Logger) {
router.get(
{
path: GET_ONBOARDING_TOKEN_ROUTE,
validate: {},
options: {
access: 'internal',
},
},
async (context, _request, response) => {
const core = await context.core;
const savedObjectsClient = core.savedObjects.getClient({ includedHiddenTypes: ['cloud'] });
const token = await getOnboardingToken(savedObjectsClient);
const body = { token };
return response.ok({
body,
headers: { 'content-type': 'application/json' },
});
}
);
}