mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Search] Add a search guide selector to index onboarding (#206810)
## Summary This adds a guide selector to the Kibana index management onboarding experience. It also fixes a bug where useQuery was causing us to re-render the page unnecessarily. <img width="1284" alt="Screenshot 2025-01-15 at 16 11 48" src="https://github.com/user-attachments/assets/19abe86f-3148-442a-8e1e-8b6b8eeb2ba1" /> ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: Rodney Norris <rodney@tattdcodemonkey.com>
This commit is contained in:
parent
ba0aa3ff43
commit
63fc1eae9f
29 changed files with 663 additions and 235 deletions
|
@ -11,12 +11,15 @@ export enum AnalyticsEvents {
|
|||
startPageShowCreateIndexUIClick = 'start_page_show_create_index_ui',
|
||||
startCreateIndexPageModifyIndexName = 'start_modify_index_name',
|
||||
startCreateIndexClick = 'start_create_index',
|
||||
startCreateIndexWorkflowSelect = 'start_workflow_select',
|
||||
startCreateIndexLanguageSelect = 'start_code_lang_select',
|
||||
startCreateIndexRunInConsole = 'start_cta_run_in_console',
|
||||
startCreateIndexCodeCopyInstall = 'start_code_copy_install',
|
||||
startCreateIndexCodeCopy = 'start_code_copy',
|
||||
startCreateIndexCreatedRedirect = 'start_index_created_api',
|
||||
startFileUploadClick = 'start_file_upload',
|
||||
indexDetailsCodeLanguageSelect = 'index_details_code_lang_select',
|
||||
indexDetailsWorkflowSelect = 'index_details_workflow_select',
|
||||
indexDetailsInstallCodeCopy = 'index_details_code_copy_install',
|
||||
indexDetailsAddMappingsCodeCopy = 'index_details_add_mappings_code_copy',
|
||||
indexDetailsIngestDocumentsCodeCopy = 'index_details_ingest_documents_code_copy',
|
||||
|
@ -29,6 +32,7 @@ export enum AnalyticsEvents {
|
|||
createIndexPageModifyIndexName = 'create_index_modify_index_name',
|
||||
createIndexCreateIndexClick = 'create_index_click_create',
|
||||
createIndexLanguageSelect = 'create_index_code_lang_select',
|
||||
createIndexWorkflowSelect = 'create_index_workflow_select',
|
||||
createIndexRunInConsole = 'create_index_run_in_console',
|
||||
createIndexCodeCopyInstall = 'create_index_copy_install',
|
||||
createIndexCodeCopy = 'create_index_code_copy',
|
||||
|
|
|
@ -34,3 +34,31 @@ export const CONNECT_CREATE_VECTOR_INDEX_CMD_DESCRIPTION = i18n.translate(
|
|||
defaultMessage: 'Use the Elasticsearch client to create an index with dense vector fields',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONNECT_CREATE_DEFAULT_INDEX_CMD_TITLE = i18n.translate(
|
||||
'xpack.searchIndices.code.createIndexCommand.title',
|
||||
{
|
||||
defaultMessage: 'Create an index with text fields',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONNECT_CREATE_DEFAULT_INDEX_CMD_DESCRIPTION = i18n.translate(
|
||||
'xpack.searchIndices.code.createIndexCommand.description',
|
||||
{
|
||||
defaultMessage: 'Use the Elasticsearch client to create an index with text fields',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONNECT_CREATE_SEMANTIC_INDEX_CMD_TITLE = i18n.translate(
|
||||
'xpack.searchIndices.code.createIndexCommand.title',
|
||||
{
|
||||
defaultMessage: 'Create an index with semantic fields',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONNECT_CREATE_SEMANTIC_INDEX_CMD_DESCRIPTION = i18n.translate(
|
||||
'xpack.searchIndices.code.createIndexCommand.description',
|
||||
{
|
||||
defaultMessage: 'Use the Elasticsearch client to create an index with semantic fields',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
|
||||
import { CreateIndexCodeExamples } from '../types';
|
||||
import {
|
||||
CONNECT_CREATE_DEFAULT_INDEX_CMD_DESCRIPTION,
|
||||
CONNECT_CREATE_DEFAULT_INDEX_CMD_TITLE,
|
||||
CONNECT_CREATE_SEMANTIC_INDEX_CMD_DESCRIPTION,
|
||||
CONNECT_CREATE_SEMANTIC_INDEX_CMD_TITLE,
|
||||
CONNECT_CREATE_VECTOR_INDEX_CMD_DESCRIPTION,
|
||||
CONNECT_CREATE_VECTOR_INDEX_CMD_TITLE,
|
||||
INSTALL_INSTRUCTIONS_DESCRIPTION,
|
||||
|
@ -14,23 +18,23 @@ import {
|
|||
} from './constants';
|
||||
|
||||
import { CurlCreateIndexExamples } from './curl';
|
||||
import { JavascriptServerlessCreateIndexExamples } from './javascript';
|
||||
import { PythonServerlessCreateIndexExamples } from './python';
|
||||
import { JavascriptCreateIndexExamples } from './javascript';
|
||||
import { PythonCreateIndexExamples } from './python';
|
||||
import { ConsoleCreateIndexExamples } from './sense';
|
||||
|
||||
export const DefaultServerlessCodeExamples: CreateIndexCodeExamples = {
|
||||
export const DefaultCodeExamples: CreateIndexCodeExamples = {
|
||||
exampleType: 'search',
|
||||
installTitle: INSTALL_INSTRUCTIONS_TITLE,
|
||||
installDescription: INSTALL_INSTRUCTIONS_DESCRIPTION,
|
||||
createIndexTitle: CONNECT_CREATE_VECTOR_INDEX_CMD_TITLE,
|
||||
createIndexDescription: CONNECT_CREATE_VECTOR_INDEX_CMD_DESCRIPTION,
|
||||
createIndexTitle: CONNECT_CREATE_DEFAULT_INDEX_CMD_TITLE,
|
||||
createIndexDescription: CONNECT_CREATE_DEFAULT_INDEX_CMD_DESCRIPTION,
|
||||
sense: ConsoleCreateIndexExamples.default,
|
||||
curl: CurlCreateIndexExamples.default,
|
||||
python: PythonServerlessCreateIndexExamples.default,
|
||||
javascript: JavascriptServerlessCreateIndexExamples.default,
|
||||
python: PythonCreateIndexExamples.default,
|
||||
javascript: JavascriptCreateIndexExamples.default,
|
||||
};
|
||||
|
||||
export const DenseVectorSeverlessCodeExamples: CreateIndexCodeExamples = {
|
||||
export const DenseVectorCodeExamples: CreateIndexCodeExamples = {
|
||||
exampleType: 'vector',
|
||||
installTitle: INSTALL_INSTRUCTIONS_TITLE,
|
||||
installDescription: INSTALL_INSTRUCTIONS_DESCRIPTION,
|
||||
|
@ -38,6 +42,18 @@ export const DenseVectorSeverlessCodeExamples: CreateIndexCodeExamples = {
|
|||
createIndexDescription: CONNECT_CREATE_VECTOR_INDEX_CMD_DESCRIPTION,
|
||||
sense: ConsoleCreateIndexExamples.dense_vector,
|
||||
curl: CurlCreateIndexExamples.dense_vector,
|
||||
python: PythonServerlessCreateIndexExamples.dense_vector,
|
||||
javascript: JavascriptServerlessCreateIndexExamples.dense_vector,
|
||||
python: PythonCreateIndexExamples.dense_vector,
|
||||
javascript: JavascriptCreateIndexExamples.dense_vector,
|
||||
};
|
||||
|
||||
export const SemanticCodeExamples: CreateIndexCodeExamples = {
|
||||
exampleType: 'semantic',
|
||||
installTitle: INSTALL_INSTRUCTIONS_TITLE,
|
||||
installDescription: INSTALL_INSTRUCTIONS_DESCRIPTION,
|
||||
createIndexTitle: CONNECT_CREATE_SEMANTIC_INDEX_CMD_TITLE,
|
||||
createIndexDescription: CONNECT_CREATE_SEMANTIC_INDEX_CMD_DESCRIPTION,
|
||||
sense: ConsoleCreateIndexExamples.semantic,
|
||||
curl: CurlCreateIndexExamples.semantic,
|
||||
python: PythonCreateIndexExamples.semantic,
|
||||
javascript: JavascriptCreateIndexExamples.semantic,
|
||||
};
|
||||
|
|
|
@ -23,7 +23,16 @@ export const CurlCreateIndexExamples: CreateIndexLanguageExamples = {
|
|||
indexName ?? INDEX_PLACEHOLDER
|
||||
}' \
|
||||
--header 'Authorization: ApiKey ${apiKey ?? API_KEY_PLACEHOLDER}' \
|
||||
--header 'Content-Type: application/json'`,
|
||||
--header 'Content-Type: application/json
|
||||
--data-raw '{
|
||||
"mappings": {
|
||||
"properties":{
|
||||
"text":{
|
||||
"type":"text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}'`,
|
||||
},
|
||||
dense_vector: {
|
||||
createIndex: ({ elasticsearchURL, apiKey, indexName }) => `curl PUT '${elasticsearchURL}/${
|
||||
|
@ -43,11 +52,27 @@ export const CurlCreateIndexExamples: CreateIndexLanguageExamples = {
|
|||
}
|
||||
}
|
||||
}
|
||||
}'`,
|
||||
},
|
||||
semantic: {
|
||||
createIndex: ({ elasticsearchURL, apiKey, indexName }) => `curl PUT '${elasticsearchURL}/${
|
||||
indexName ?? INDEX_PLACEHOLDER
|
||||
}' \
|
||||
--header 'Authorization: ApiKey ${apiKey ?? API_KEY_PLACEHOLDER}' \
|
||||
--header 'Content-Type: application/json
|
||||
--data-raw '{
|
||||
"mappings": {
|
||||
"properties":{
|
||||
"text":{
|
||||
"type":"semantic_text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}'`,
|
||||
},
|
||||
};
|
||||
|
||||
export const CurlVectorsIngestDataExample: IngestDataCodeDefinition = {
|
||||
export const CurlIngestDataExample: IngestDataCodeDefinition = {
|
||||
ingestCommand: ({ elasticsearchURL, apiKey, indexName, sampleDocuments }) => {
|
||||
let result = `curl -X POST "${elasticsearchURL}/_bulk?pretty" \
|
||||
--header 'Authorization: ApiKey ${apiKey ?? API_KEY_PLACEHOLDER}' \
|
||||
|
|
|
@ -8,21 +8,43 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { IngestDataCodeExamples } from '../types';
|
||||
|
||||
import { JSServerlessIngestVectorDataExample } from './javascript';
|
||||
import { PythonServerlessVectorsIngestDataExample } from './python';
|
||||
import { ConsoleVectorsIngestDataExample } from './sense';
|
||||
import { CurlVectorsIngestDataExample } from './curl';
|
||||
import { JSIngestDataExample } from './javascript';
|
||||
import { PythonIngestDataExample } from './python';
|
||||
import { ConsoleIngestDataExample } from './sense';
|
||||
import { CurlIngestDataExample } from './curl';
|
||||
import { INSTALL_INSTRUCTIONS_TITLE, INSTALL_INSTRUCTIONS_DESCRIPTION } from './constants';
|
||||
|
||||
export const DenseVectorServerlessCodeExamples: IngestDataCodeExamples = {
|
||||
const addMappingsTitle = i18n.translate(
|
||||
'xpack.searchIndices.codeExamples.serverless.denseVector.mappingsTitle',
|
||||
{
|
||||
defaultMessage: 'Define field mappings',
|
||||
}
|
||||
);
|
||||
|
||||
export const DefaultIngestDataCodeExamples: IngestDataCodeExamples = {
|
||||
installTitle: INSTALL_INSTRUCTIONS_TITLE,
|
||||
installDescription: INSTALL_INSTRUCTIONS_DESCRIPTION,
|
||||
addMappingsTitle: i18n.translate(
|
||||
'xpack.searchIndices.codeExamples.serverless.denseVector.mappingsTitle',
|
||||
addMappingsTitle,
|
||||
addMappingsDescription: i18n.translate(
|
||||
'xpack.searchIndices.codeExamples.serverless.default.mappingsDescription',
|
||||
{
|
||||
defaultMessage: 'Define field mappings',
|
||||
defaultMessage:
|
||||
'This example defines one field: a text field that will provide full-text search capabilities. You can add more field types by modifying the mappings in your API call, or in the mappings tab.',
|
||||
}
|
||||
),
|
||||
defaultMapping: {
|
||||
text: { type: 'text' },
|
||||
},
|
||||
sense: ConsoleIngestDataExample,
|
||||
curl: CurlIngestDataExample,
|
||||
python: PythonIngestDataExample,
|
||||
javascript: JSIngestDataExample,
|
||||
};
|
||||
|
||||
export const DenseVectorIngestDataCodeExamples: IngestDataCodeExamples = {
|
||||
installTitle: INSTALL_INSTRUCTIONS_TITLE,
|
||||
installDescription: INSTALL_INSTRUCTIONS_DESCRIPTION,
|
||||
addMappingsTitle,
|
||||
addMappingsDescription: i18n.translate(
|
||||
'xpack.searchIndices.codeExamples.serverless.denseVector.mappingsDescription',
|
||||
{
|
||||
|
@ -34,8 +56,29 @@ export const DenseVectorServerlessCodeExamples: IngestDataCodeExamples = {
|
|||
vector: { type: 'dense_vector', dims: 3 },
|
||||
text: { type: 'text' },
|
||||
},
|
||||
sense: ConsoleVectorsIngestDataExample,
|
||||
curl: CurlVectorsIngestDataExample,
|
||||
python: PythonServerlessVectorsIngestDataExample,
|
||||
javascript: JSServerlessIngestVectorDataExample,
|
||||
sense: ConsoleIngestDataExample,
|
||||
curl: CurlIngestDataExample,
|
||||
python: PythonIngestDataExample,
|
||||
javascript: JSIngestDataExample,
|
||||
};
|
||||
|
||||
export const SemanticIngestDataCodeExamples: IngestDataCodeExamples = {
|
||||
installTitle: INSTALL_INSTRUCTIONS_TITLE,
|
||||
installDescription: INSTALL_INSTRUCTIONS_DESCRIPTION,
|
||||
addMappingsTitle,
|
||||
addMappingsDescription: i18n.translate(
|
||||
'xpack.searchIndices.codeExamples.serverless.denseVector.mappingsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'This example defines one field: a semantic text field that will provide vector search capabilities using the default ELSER model. You can add more field types by modifying the mappings in your API call, or in the mappings tab.',
|
||||
}
|
||||
),
|
||||
defaultMapping: {
|
||||
// @ts-expect-error - our types don't understand yet that we can have semantic_text fields without inference ids
|
||||
text: { type: 'semantic_text' },
|
||||
},
|
||||
sense: ConsoleIngestDataExample,
|
||||
curl: CurlIngestDataExample,
|
||||
python: PythonIngestDataExample,
|
||||
javascript: JSIngestDataExample,
|
||||
};
|
||||
|
|
|
@ -19,11 +19,11 @@ export const JAVASCRIPT_INFO: CodeLanguage = {
|
|||
codeBlockLanguage: 'javascript',
|
||||
};
|
||||
|
||||
const SERVERLESS_INSTALL_CMD = `npm install @elastic/elasticsearch`;
|
||||
const INSTALL_CMD = `npm install @elastic/elasticsearch`;
|
||||
|
||||
export const JavascriptServerlessCreateIndexExamples: CreateIndexLanguageExamples = {
|
||||
export const JavascriptCreateIndexExamples: CreateIndexLanguageExamples = {
|
||||
default: {
|
||||
installCommand: SERVERLESS_INSTALL_CMD,
|
||||
installCommand: INSTALL_CMD,
|
||||
createIndex: ({
|
||||
elasticsearchURL,
|
||||
apiKey,
|
||||
|
@ -39,10 +39,15 @@ const client = new Client({
|
|||
|
||||
client.indices.create({
|
||||
index: "${indexName ?? INDEX_PLACEHOLDER}",
|
||||
mappings: {
|
||||
properties: {
|
||||
text: { type: "text"}
|
||||
},
|
||||
},
|
||||
});`,
|
||||
},
|
||||
dense_vector: {
|
||||
installCommand: SERVERLESS_INSTALL_CMD,
|
||||
installCommand: INSTALL_CMD,
|
||||
createIndex: ({
|
||||
elasticsearchURL,
|
||||
apiKey,
|
||||
|
@ -64,12 +69,36 @@ client.indices.create({
|
|||
text: { type: "text"}
|
||||
},
|
||||
},
|
||||
});`,
|
||||
},
|
||||
semantic: {
|
||||
installCommand: INSTALL_CMD,
|
||||
createIndex: ({
|
||||
elasticsearchURL,
|
||||
apiKey,
|
||||
indexName,
|
||||
}) => `import { Client } from "@elastic/elasticsearch"
|
||||
|
||||
const client = new Client({
|
||||
node: '${elasticsearchURL}',
|
||||
auth: {
|
||||
apiKey: "${apiKey ?? API_KEY_PLACEHOLDER}"
|
||||
}
|
||||
});
|
||||
|
||||
client.indices.create({
|
||||
index: "${indexName ?? INDEX_PLACEHOLDER}",
|
||||
mappings: {
|
||||
properties: {
|
||||
text: { type: "semantic_text"}
|
||||
},
|
||||
},
|
||||
});`,
|
||||
},
|
||||
};
|
||||
|
||||
export const JSServerlessIngestVectorDataExample: IngestDataCodeDefinition = {
|
||||
installCommand: SERVERLESS_INSTALL_CMD,
|
||||
export const JSIngestDataExample: IngestDataCodeDefinition = {
|
||||
installCommand: INSTALL_CMD,
|
||||
ingestCommand: ({
|
||||
apiKey,
|
||||
elasticsearchURL,
|
||||
|
|
|
@ -23,11 +23,11 @@ export const PYTHON_INFO: CodeLanguage = {
|
|||
codeBlockLanguage: 'python',
|
||||
};
|
||||
|
||||
const SERVERLESS_PYTHON_INSTALL_CMD = 'pip install elasticsearch';
|
||||
const PYTHON_INSTALL_CMD = 'pip install elasticsearch';
|
||||
|
||||
export const PythonServerlessCreateIndexExamples: CreateIndexLanguageExamples = {
|
||||
export const PythonCreateIndexExamples: CreateIndexLanguageExamples = {
|
||||
default: {
|
||||
installCommand: SERVERLESS_PYTHON_INSTALL_CMD,
|
||||
installCommand: PYTHON_INSTALL_CMD,
|
||||
createIndex: ({
|
||||
elasticsearchURL,
|
||||
apiKey,
|
||||
|
@ -40,11 +40,16 @@ client = Elasticsearch(
|
|||
)
|
||||
|
||||
client.indices.create(
|
||||
index="${indexName ?? INDEX_PLACEHOLDER}"
|
||||
index="${indexName ?? INDEX_PLACEHOLDER}",
|
||||
mappings={
|
||||
"properties": {
|
||||
"text": {"type": "text"}
|
||||
}
|
||||
}
|
||||
)`,
|
||||
},
|
||||
dense_vector: {
|
||||
installCommand: SERVERLESS_PYTHON_INSTALL_CMD,
|
||||
installCommand: PYTHON_INSTALL_CMD,
|
||||
createIndex: ({
|
||||
elasticsearchURL,
|
||||
apiKey,
|
||||
|
@ -64,10 +69,32 @@ client.indices.create(
|
|||
"text": {"type": "text"}
|
||||
}
|
||||
}
|
||||
)`,
|
||||
},
|
||||
semantic: {
|
||||
installCommand: PYTHON_INSTALL_CMD,
|
||||
createIndex: ({
|
||||
elasticsearchURL,
|
||||
apiKey,
|
||||
indexName,
|
||||
}: CodeSnippetParameters) => `from elasticsearch import Elasticsearch
|
||||
|
||||
client = Elasticsearch(
|
||||
"${elasticsearchURL}",
|
||||
api_key="${apiKey ?? API_KEY_PLACEHOLDER}"
|
||||
)
|
||||
|
||||
client.indices.create(
|
||||
index="${indexName ?? INDEX_PLACEHOLDER}",
|
||||
mappings={
|
||||
"properties": {
|
||||
"text": {"type": "semantic_text"}
|
||||
}
|
||||
}
|
||||
)`,
|
||||
},
|
||||
};
|
||||
const serverlessIngestionCommand: IngestCodeSnippetFunction = ({
|
||||
const ingestionCommand: IngestCodeSnippetFunction = ({
|
||||
elasticsearchURL,
|
||||
apiKey,
|
||||
indexName,
|
||||
|
@ -86,7 +113,7 @@ docs = ${JSON.stringify(sampleDocuments, null, 4)}
|
|||
bulk_response = helpers.bulk(client, docs, index=index_name)
|
||||
print(bulk_response)`;
|
||||
|
||||
const serverlessUpdateMappingsCommand: IngestCodeSnippetFunction = ({
|
||||
const updateMappingsCommand: IngestCodeSnippetFunction = ({
|
||||
elasticsearchURL,
|
||||
apiKey,
|
||||
indexName,
|
||||
|
@ -106,8 +133,8 @@ mapping_response = client.indices.put_mapping(index=index_name, body=mappings)
|
|||
print(mapping_response)
|
||||
`;
|
||||
|
||||
export const PythonServerlessVectorsIngestDataExample: IngestDataCodeDefinition = {
|
||||
installCommand: SERVERLESS_PYTHON_INSTALL_CMD,
|
||||
ingestCommand: serverlessIngestionCommand,
|
||||
updateMappingsCommand: serverlessUpdateMappingsCommand,
|
||||
export const PythonIngestDataExample: IngestDataCodeDefinition = {
|
||||
installCommand: PYTHON_INSTALL_CMD,
|
||||
ingestCommand: ingestionCommand,
|
||||
updateMappingsCommand,
|
||||
};
|
||||
|
|
|
@ -11,7 +11,16 @@ import { CreateIndexLanguageExamples } from './types';
|
|||
|
||||
export const ConsoleCreateIndexExamples: CreateIndexLanguageExamples = {
|
||||
default: {
|
||||
createIndex: ({ indexName }) => `PUT /${indexName ?? INDEX_PLACEHOLDER}`,
|
||||
createIndex: ({ indexName }) => `PUT /${indexName ?? INDEX_PLACEHOLDER}
|
||||
{
|
||||
"mappings": {
|
||||
"properties":{
|
||||
"text":{
|
||||
"type":"text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
dense_vector: {
|
||||
createIndex: ({ indexName }) => `PUT /${indexName ?? INDEX_PLACEHOLDER}
|
||||
|
@ -27,11 +36,23 @@ export const ConsoleCreateIndexExamples: CreateIndexLanguageExamples = {
|
|||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
semantic: {
|
||||
createIndex: ({ indexName }) => `PUT /${indexName ?? INDEX_PLACEHOLDER}
|
||||
{
|
||||
"mappings": {
|
||||
"properties":{
|
||||
"text":{
|
||||
"type":"semantic_text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
};
|
||||
|
||||
export const ConsoleVectorsIngestDataExample: IngestDataCodeDefinition = {
|
||||
export const ConsoleIngestDataExample: IngestDataCodeDefinition = {
|
||||
ingestCommand: ({ indexName, sampleDocuments }) => {
|
||||
let result = 'POST /_bulk?pretty\n';
|
||||
sampleDocuments.forEach((document) => {
|
||||
|
|
|
@ -10,8 +10,11 @@ import { CreateIndexCodeDefinition, IngestDataCodeDefinition } from '../types';
|
|||
export interface CreateIndexLanguageExamples {
|
||||
default: CreateIndexCodeDefinition;
|
||||
dense_vector: CreateIndexCodeDefinition;
|
||||
semantic: CreateIndexCodeDefinition;
|
||||
}
|
||||
|
||||
export interface IngestDataLanguageExamples {
|
||||
default: IngestDataCodeDefinition;
|
||||
dense_vector: IngestDataCodeDefinition;
|
||||
semantic: IngestDataCodeDefinition;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export type WorkflowId = 'default' | 'vector' | 'semantic';
|
||||
|
||||
export interface Workflow {
|
||||
title: string;
|
||||
id: WorkflowId;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export const workflows: Workflow[] = [
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.workflows.default', {
|
||||
defaultMessage: 'Keyword Search',
|
||||
}),
|
||||
id: 'default',
|
||||
summary: i18n.translate('xpack.searchIndices.workflows.defaultSummary', {
|
||||
defaultMessage: 'Set up an index in Elasticsearch using the text field mapping.',
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.workflows.vector', {
|
||||
defaultMessage: 'Vector Search',
|
||||
}),
|
||||
id: 'vector',
|
||||
summary: i18n.translate('xpack.searchIndices.workflows.vectorSummary', {
|
||||
defaultMessage: 'Set up an index in Elasticsearch using the dense_vector field mapping.',
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.searchIndices.workflows.semantic', {
|
||||
defaultMessage: 'Semantic Search',
|
||||
}),
|
||||
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.",
|
||||
}),
|
||||
},
|
||||
];
|
|
@ -22,6 +22,8 @@ import { CreateIndexPanel } from '../shared/create_index_panel';
|
|||
|
||||
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() {
|
||||
const defaultIndexName = generateRandomIndexName();
|
||||
|
@ -50,6 +52,7 @@ export const CreateIndex = ({ indicesData }: CreateIndexProps) => {
|
|||
? CreateIndexViewMode.Code
|
||||
: CreateIndexViewMode.UI
|
||||
);
|
||||
const { workflow, setSelectedWorkflowId } = useWorkflow();
|
||||
const usageTracker = useUsageTracker();
|
||||
const onChangeView = useCallback(
|
||||
(id: string) => {
|
||||
|
@ -102,6 +105,14 @@ export const CreateIndex = ({ indicesData }: CreateIndexProps) => {
|
|||
selectedLanguage={formState.codingLanguage}
|
||||
indexName={formState.indexName}
|
||||
changeCodingLanguage={onChangeCodingLanguage}
|
||||
changeWorkflowId={(workflowId: WorkflowId) => {
|
||||
setSelectedWorkflowId(workflowId);
|
||||
usageTracker.click([
|
||||
AnalyticsEvents.startCreateIndexWorkflowSelect,
|
||||
`${AnalyticsEvents.startCreateIndexWorkflowSelect}_${workflowId}`,
|
||||
]);
|
||||
}}
|
||||
selectedWorkflow={workflow}
|
||||
canCreateApiKey={userPrivileges?.privileges.canCreateApiKeys}
|
||||
analyticsEvents={{
|
||||
runInConsole: AnalyticsEvents.createIndexRunInConsole,
|
||||
|
|
|
@ -7,11 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import {
|
||||
AddDocumentsCodeExample,
|
||||
basicExampleTexts,
|
||||
exampleTextsWithCustomMapping,
|
||||
} from './add_documents_code_example';
|
||||
import { AddDocumentsCodeExample, exampleTexts } from './add_documents_code_example';
|
||||
import { generateSampleDocument } from '../../utils/document_generation';
|
||||
import { MappingProperty } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
|
@ -71,7 +67,7 @@ describe('AddDocumentsCodeExample', () => {
|
|||
|
||||
expect(generateSampleDocument).toHaveBeenCalledTimes(3);
|
||||
|
||||
basicExampleTexts.forEach((text, index) => {
|
||||
exampleTexts.forEach((text, index) => {
|
||||
expect(generateSampleDocument).toHaveBeenNthCalledWith(index + 1, mappingProperties, text);
|
||||
});
|
||||
});
|
||||
|
@ -84,16 +80,15 @@ describe('AddDocumentsCodeExample', () => {
|
|||
expect(generateSampleDocument).toHaveBeenCalledTimes(3);
|
||||
|
||||
const mappingProperties: Record<string, MappingProperty> = {
|
||||
vector: { type: 'dense_vector', dims: 3 },
|
||||
text: { type: 'text' },
|
||||
};
|
||||
|
||||
basicExampleTexts.forEach((text, index) => {
|
||||
exampleTexts.forEach((text, index) => {
|
||||
expect(generateSampleDocument).toHaveBeenNthCalledWith(index + 1, mappingProperties, text);
|
||||
});
|
||||
});
|
||||
|
||||
it('pass basic examples when mapping is default with extra vector fields', () => {
|
||||
it('pass examples when mapping is default with extra vector fields', () => {
|
||||
const indexName = 'test-index';
|
||||
const mappingProperties: Record<string, MappingProperty> = {
|
||||
vector: { type: 'dense_vector', dims: 3, similarity: 'extra' },
|
||||
|
@ -106,25 +101,7 @@ describe('AddDocumentsCodeExample', () => {
|
|||
|
||||
expect(generateSampleDocument).toHaveBeenCalledTimes(3);
|
||||
|
||||
basicExampleTexts.forEach((text, index) => {
|
||||
expect(generateSampleDocument).toHaveBeenNthCalledWith(index + 1, mappingProperties, text);
|
||||
});
|
||||
});
|
||||
|
||||
it('pass examples text when mapping is custom', () => {
|
||||
const indexName = 'test-index';
|
||||
const mappingProperties: Record<string, MappingProperty> = {
|
||||
text: { type: 'text' },
|
||||
test: { type: 'boolean' },
|
||||
};
|
||||
|
||||
render(
|
||||
<AddDocumentsCodeExample indexName={indexName} mappingProperties={mappingProperties} />
|
||||
);
|
||||
|
||||
expect(generateSampleDocument).toHaveBeenCalledTimes(3);
|
||||
|
||||
exampleTextsWithCustomMapping.forEach((text, index) => {
|
||||
exampleTexts.forEach((text, index) => {
|
||||
expect(generateSampleDocument).toHaveBeenNthCalledWith(index + 1, mappingProperties, text);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,17 +6,15 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { MappingDenseVectorProperty, MappingProperty } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import { MappingProperty } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TryInConsoleButton } from '@kbn/try-in-console';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { useSearchApiKey } from '@kbn/search-api-keys-components';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { IngestCodeSnippetParameters } from '../../types';
|
||||
import { LanguageSelector } from '../shared/language_selector';
|
||||
import { useIngestCodeExamples } from './hooks/use_ingest_code_examples';
|
||||
import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url';
|
||||
import { useUsageTracker } from '../../contexts/usage_tracker_context';
|
||||
import { AvailableLanguages, LanguageOptions, Languages } from '../../code_examples';
|
||||
|
@ -24,13 +22,15 @@ import { AnalyticsEvents } from '../../analytics/constants';
|
|||
import { CodeSample } from '../shared/code_sample';
|
||||
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 basicExampleTexts = [
|
||||
'Yellowstone National Park',
|
||||
'Yosemite National Park',
|
||||
'Rocky Mountain National Park',
|
||||
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.',
|
||||
'Yosemite National Park is a United States National Park, covering over 750,000 acres of land in California. A UNESCO World Heritage Site, the park is best known for its granite cliffs, waterfalls and giant sequoia trees. Yosemite hosts over four million visitors in most years, with a peak of five million visitors in 2016. The park is home to a diverse range of wildlife, including mule deer, black bears, and the endangered Sierra Nevada bighorn sheep. The park has 1,200 square miles of wilderness, and is a popular destination for rock climbers, with over 3,000 feet of vertical granite to climb. Its most famous and cliff is the El Capitan, a 3,000 feet monolith along its tallest face.',
|
||||
'Rocky Mountain National Park is one of the most popular national parks in the United States. It receives over 4.5 million visitors annually, and is known for its mountainous terrain, including Longs Peak, which is the highest peak in the park. The park is home to a variety of wildlife, including elk, mule deer, moose, and bighorn sheep. The park is also home to a variety of ecosystems, including montane, subalpine, and alpine tundra. The park is a popular destination for hiking, camping, and wildlife viewing, and is a UNESCO World Heritage Site.',
|
||||
];
|
||||
export const exampleTextsWithCustomMapping = [1, 2, 3].map((num) => `Example text ${num}`);
|
||||
|
||||
export interface AddDocumentsCodeExampleProps {
|
||||
indexName: string;
|
||||
|
@ -42,41 +42,28 @@ export const AddDocumentsCodeExample = ({
|
|||
mappingProperties,
|
||||
}: AddDocumentsCodeExampleProps) => {
|
||||
const { application, share, console: consolePlugin } = useKibana().services;
|
||||
const ingestCodeExamples = useIngestCodeExamples();
|
||||
const elasticsearchUrl = useElasticsearchUrl();
|
||||
const usageTracker = useUsageTracker();
|
||||
const indexHasMappings = Object.keys(mappingProperties).length > 0;
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] =
|
||||
useState<AvailableLanguages>(getDefaultCodingLanguage);
|
||||
const selectedCodeExamples = ingestCodeExamples[selectedLanguage];
|
||||
const codeSampleMappings = indexHasMappings
|
||||
? mappingProperties
|
||||
: ingestCodeExamples.defaultMapping;
|
||||
const { selectedWorkflowId, setSelectedWorkflowId, ingestExamples, workflow } = useWorkflow();
|
||||
const selectedCodeExamples = ingestExamples[selectedLanguage];
|
||||
const codeSampleMappings = indexHasMappings ? mappingProperties : ingestExamples.defaultMapping;
|
||||
const onSelectLanguage = useCallback(
|
||||
(value: AvailableLanguages) => {
|
||||
setSelectedLanguage(value);
|
||||
usageTracker.count([
|
||||
AnalyticsEvents.startCreateIndexLanguageSelect,
|
||||
`${AnalyticsEvents.startCreateIndexLanguageSelect}_${value}`,
|
||||
AnalyticsEvents.indexDetailsCodeLanguageSelect,
|
||||
`${AnalyticsEvents.indexDetailsCodeLanguageSelect}_${value}`,
|
||||
]);
|
||||
},
|
||||
[usageTracker]
|
||||
);
|
||||
const sampleDocuments = useMemo(() => {
|
||||
// If the default mapping was used, we need to exclude generated vector fields
|
||||
const copyCodeSampleMappings = {
|
||||
...codeSampleMappings,
|
||||
vector: {
|
||||
type: codeSampleMappings.vector?.type,
|
||||
dims: (codeSampleMappings.vector as MappingDenseVectorProperty)?.dims,
|
||||
},
|
||||
};
|
||||
const isDefaultMapping = isEqual(copyCodeSampleMappings, ingestCodeExamples.defaultMapping);
|
||||
const sampleTexts = isDefaultMapping ? basicExampleTexts : exampleTextsWithCustomMapping;
|
||||
|
||||
return sampleTexts.map((text) => generateSampleDocument(codeSampleMappings, text));
|
||||
}, [codeSampleMappings, ingestCodeExamples.defaultMapping]);
|
||||
return exampleTexts.map((text) => generateSampleDocument(codeSampleMappings, text));
|
||||
}, [codeSampleMappings]);
|
||||
const { apiKey } = useSearchApiKey();
|
||||
const codeParams: IngestCodeSnippetParameters = useMemo(() => {
|
||||
return {
|
||||
|
@ -88,6 +75,7 @@ export const AddDocumentsCodeExample = ({
|
|||
apiKey: apiKey || undefined,
|
||||
};
|
||||
}, [indexName, elasticsearchUrl, sampleDocuments, codeSampleMappings, indexHasMappings, apiKey]);
|
||||
const [panelRef, setPanelRef] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
|
@ -95,24 +83,34 @@ export const AddDocumentsCodeExample = ({
|
|||
hasShadow={false}
|
||||
paddingSize="m"
|
||||
data-test-subj="SearchIndicesAddDocumentsCode"
|
||||
panelRef={setPanelRef}
|
||||
>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem css={{ maxWidth: '300px' }}>
|
||||
<LanguageSelector
|
||||
options={LanguageOptions}
|
||||
selectedLanguage={selectedLanguage}
|
||||
onSelectLanguage={onSelectLanguage}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{!indexHasMappings && (
|
||||
<EuiFlexItem css={{ maxWidth: '300px' }} grow={false}>
|
||||
<GuideSelector
|
||||
selectedWorkflowId={selectedWorkflowId}
|
||||
onChange={(workflowId: WorkflowId) => {
|
||||
setSelectedWorkflowId(workflowId);
|
||||
usageTracker.click([
|
||||
AnalyticsEvents.indexDetailsCodeLanguageSelect,
|
||||
`${AnalyticsEvents.indexDetailsCodeLanguageSelect}_${workflowId}`,
|
||||
]);
|
||||
}}
|
||||
showTour
|
||||
container={panelRef}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<TryInConsoleButton
|
||||
request={
|
||||
!indexHasMappings
|
||||
? `${ingestCodeExamples.sense.updateMappingsCommand(
|
||||
? `${ingestExamples.sense.updateMappingsCommand(
|
||||
codeParams
|
||||
)}\n\n${ingestCodeExamples.sense.ingestCommand(codeParams)}`
|
||||
: ingestCodeExamples.sense.ingestCommand(codeParams)
|
||||
)}\n\n${ingestExamples.sense.ingestCommand(codeParams)}`
|
||||
: ingestExamples.sense.ingestCommand(codeParams)
|
||||
}
|
||||
application={application}
|
||||
sharePlugin={share}
|
||||
|
@ -120,12 +118,31 @@ export const AddDocumentsCodeExample = ({
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{!!workflow && (
|
||||
<EuiFlexItem>
|
||||
<EuiTitle>
|
||||
<h3>{workflow.title}</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText>
|
||||
<p>{workflow.summary}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem css={{ maxWidth: '300px' }} grow={false}>
|
||||
<LanguageSelector
|
||||
options={LanguageOptions}
|
||||
selectedLanguage={selectedLanguage}
|
||||
onSelectLanguage={onSelectLanguage}
|
||||
showLabel
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{selectedCodeExamples.installCommand && (
|
||||
<EuiFlexItem>
|
||||
<CodeSample
|
||||
id="installCodeExample"
|
||||
title={ingestCodeExamples.installTitle}
|
||||
description={ingestCodeExamples.installDescription}
|
||||
title={ingestExamples.installTitle}
|
||||
description={ingestExamples.installDescription}
|
||||
language="shell"
|
||||
code={selectedCodeExamples.installCommand}
|
||||
onCodeCopyClick={() => {
|
||||
|
@ -141,8 +158,8 @@ export const AddDocumentsCodeExample = ({
|
|||
<EuiFlexItem>
|
||||
<CodeSample
|
||||
id="addMappingsCodeExample"
|
||||
title={ingestCodeExamples.addMappingsTitle}
|
||||
description={ingestCodeExamples.addMappingsDescription}
|
||||
title={ingestExamples.addMappingsTitle}
|
||||
description={ingestExamples.addMappingsDescription}
|
||||
language={Languages[selectedLanguage].codeBlockLanguage}
|
||||
code={selectedCodeExamples.updateMappingsCommand(codeParams)}
|
||||
onCodeCopyClick={() => {
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* 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 * as IngestCodeExamples from '../../../code_examples/ingest_data';
|
||||
|
||||
export const useIngestCodeExamples = () => {
|
||||
// TODO: Choose code examples based on onboarding token, stack vs es3, or project type
|
||||
return IngestCodeExamples.DenseVectorServerlessCodeExamples;
|
||||
};
|
|
@ -79,12 +79,7 @@ export const SearchIndexDetailsPage = () => {
|
|||
}, [share, index]);
|
||||
const navigateToDiscover = useNavigateToDiscover(indexName);
|
||||
|
||||
const [hasDocuments, setHasDocuments] = useState<boolean>(false);
|
||||
const [isDocumentsLoading, setDocumentsLoading] = useState<boolean>(true);
|
||||
useEffect(() => {
|
||||
setDocumentsLoading(isInitialLoading);
|
||||
setHasDocuments(!(!isInitialLoading && indexDocuments?.results?.data.length === 0));
|
||||
}, [indexDocuments, isInitialLoading, setHasDocuments, setDocumentsLoading]);
|
||||
const hasDocuments = Boolean(isInitialLoading || indexDocuments?.results?.data.length);
|
||||
|
||||
usePageChrome(indexName, [
|
||||
...IndexManagementBreadcrumbs,
|
||||
|
@ -225,7 +220,7 @@ export const SearchIndexDetailsPage = () => {
|
|||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty
|
||||
isLoading={isDocumentsLoading}
|
||||
isLoading={isInitialLoading}
|
||||
data-test-subj="viewInDiscoverLink"
|
||||
onClick={navigateToDiscover}
|
||||
>
|
||||
|
@ -237,7 +232,7 @@ export const SearchIndexDetailsPage = () => {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
isLoading={isDocumentsLoading}
|
||||
isLoading={isInitialLoading}
|
||||
data-test-subj="useInPlaygroundLink"
|
||||
onClick={navigateToPlayground}
|
||||
iconType="launch"
|
||||
|
@ -255,7 +250,7 @@ export const SearchIndexDetailsPage = () => {
|
|||
<EuiButtonEmpty
|
||||
href={docLinks.links.apiReference}
|
||||
target="_blank"
|
||||
isLoading={isDocumentsLoading}
|
||||
isLoading={isInitialLoading}
|
||||
iconType="documentation"
|
||||
data-test-subj="ApiReferenceDoc"
|
||||
>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { TryInConsoleButton } from '@kbn/try-in-console';
|
||||
|
||||
import { useSearchApiKey } from '@kbn/search-api-keys-components';
|
||||
|
@ -17,13 +17,17 @@ import { useElasticsearchUrl } from '../../hooks/use_elasticsearch_url';
|
|||
|
||||
import { APIKeyCallout } from './api_key_callout';
|
||||
import { CodeSample } from './code_sample';
|
||||
import { useCreateIndexCodingExamples } from './hooks/use_create_index_coding_examples';
|
||||
import { useWorkflow } from './hooks/use_workflow';
|
||||
import { LanguageSelector } from './language_selector';
|
||||
import { GuideSelector } from './guide_selector';
|
||||
import { Workflow, WorkflowId } from '../../code_examples/workflows';
|
||||
|
||||
export interface CreateIndexCodeViewProps {
|
||||
selectedLanguage: AvailableLanguages;
|
||||
indexName: string;
|
||||
changeCodingLanguage: (language: AvailableLanguages) => void;
|
||||
changeWorkflowId: (workflowId: WorkflowId) => void;
|
||||
selectedWorkflow?: Workflow;
|
||||
canCreateApiKey?: boolean;
|
||||
analyticsEvents: {
|
||||
runInConsole: string;
|
||||
|
@ -36,12 +40,14 @@ export const CreateIndexCodeView = ({
|
|||
analyticsEvents,
|
||||
canCreateApiKey,
|
||||
changeCodingLanguage,
|
||||
changeWorkflowId,
|
||||
selectedWorkflow,
|
||||
indexName,
|
||||
selectedLanguage,
|
||||
}: CreateIndexCodeViewProps) => {
|
||||
const { application, share, console: consolePlugin } = useKibana().services;
|
||||
const usageTracker = useUsageTracker();
|
||||
const selectedCodeExamples = useCreateIndexCodingExamples();
|
||||
const { createIndexExamples: selectedCodeExamples } = useWorkflow();
|
||||
|
||||
const elasticsearchUrl = useElasticsearchUrl();
|
||||
const { apiKey } = useSearchApiKey();
|
||||
|
@ -64,30 +70,52 @@ export const CreateIndexCodeView = ({
|
|||
<APIKeyCallout apiKey={apiKey} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem 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>
|
||||
<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>
|
||||
</EuiFlexItem>
|
||||
{!!selectedWorkflow && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h4>{selectedWorkflow?.title}</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>{selectedWorkflow?.summary}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
<EuiFlexItem css={{ maxWidth: '300px' }}>
|
||||
<LanguageSelector
|
||||
options={LanguageOptions}
|
||||
selectedLanguage={selectedLanguage}
|
||||
onSelectLanguage={changeCodingLanguage}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{selectedCodeExample.installCommand && (
|
||||
<CodeSample
|
||||
title={selectedCodeExamples.installTitle}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 React, { useState, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCard,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiTourStep,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Workflow, 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;
|
||||
showTour: boolean;
|
||||
container?: HTMLElement | null;
|
||||
}
|
||||
|
||||
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={
|
||||
<EuiText>
|
||||
<p>
|
||||
{i18n.translate('xpack.searchIndices.tourDescription', {
|
||||
defaultMessage: 'Explore additional guides for setting up your Elasticsearch index.',
|
||||
})}
|
||||
</p>
|
||||
</EuiText>
|
||||
}
|
||||
isStepOpen={tourIsOpen}
|
||||
minWidth={300}
|
||||
onFinish={() => setTourIsOpen(false)}
|
||||
step={1}
|
||||
stepsTotal={1}
|
||||
title={i18n.translate('xpack.searchIndices.touTitle', {
|
||||
defaultMessage: 'New guides available!',
|
||||
})}
|
||||
anchorPosition="rightUp"
|
||||
>
|
||||
<Popover />
|
||||
</EuiTourStep>
|
||||
) : (
|
||||
<Popover />
|
||||
);
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* 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 { CreateIndexCodeExamples } from '../../../types';
|
||||
import { DenseVectorSeverlessCodeExamples } from '../../../code_examples/create_index';
|
||||
|
||||
export const useCreateIndexCodingExamples = (): CreateIndexCodeExamples => {
|
||||
// TODO: in the future this will be dynamic based on the onboarding token
|
||||
// or project sub-type
|
||||
return DenseVectorSeverlessCodeExamples;
|
||||
};
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
const GUIDE_TOUR_KEY = 'searchIndicesIngestDataGuideTour';
|
||||
|
||||
export const useGuideTour = () => {
|
||||
const hasDismissedGuide = localStorage.getItem(GUIDE_TOUR_KEY) === 'dismissed';
|
||||
const [tourIsOpen, setTourIsOpen] = useState(!hasDismissedGuide);
|
||||
return {
|
||||
tourIsOpen,
|
||||
setTourIsOpen: (isOpen: boolean) => {
|
||||
setTourIsOpen(isOpen);
|
||||
localStorage.setItem(GUIDE_TOUR_KEY, isOpen ? '' : 'dismissed');
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { useState } from 'react';
|
||||
import {
|
||||
DenseVectorIngestDataCodeExamples,
|
||||
SemanticIngestDataCodeExamples,
|
||||
DefaultIngestDataCodeExamples,
|
||||
} from '../../../code_examples/ingest_data';
|
||||
import { WorkflowId, workflows } from '../../../code_examples/workflows';
|
||||
import {
|
||||
DefaultCodeExamples,
|
||||
DenseVectorCodeExamples,
|
||||
SemanticCodeExamples,
|
||||
} from '../../../code_examples/create_index';
|
||||
|
||||
const workflowIdToCreateIndexExamples = (type: WorkflowId) => {
|
||||
switch (type) {
|
||||
case 'vector':
|
||||
return DenseVectorCodeExamples;
|
||||
case 'semantic':
|
||||
return SemanticCodeExamples;
|
||||
default:
|
||||
return DefaultCodeExamples;
|
||||
}
|
||||
};
|
||||
|
||||
const workflowIdToIngestDataExamples = (type: WorkflowId) => {
|
||||
switch (type) {
|
||||
case 'vector':
|
||||
return DenseVectorIngestDataCodeExamples;
|
||||
case 'semantic':
|
||||
return SemanticIngestDataCodeExamples;
|
||||
default:
|
||||
return DefaultIngestDataCodeExamples;
|
||||
}
|
||||
};
|
||||
|
||||
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');
|
||||
return {
|
||||
selectedWorkflowId,
|
||||
setSelectedWorkflowId,
|
||||
workflow: workflows.find((workflow) => workflow.id === selectedWorkflowId),
|
||||
createIndexExamples: workflowIdToCreateIndexExamples(selectedWorkflowId),
|
||||
ingestExamples: workflowIdToIngestDataExamples(selectedWorkflowId),
|
||||
};
|
||||
};
|
|
@ -17,12 +17,14 @@ export interface LanguageSelectorProps {
|
|||
selectedLanguage: AvailableLanguages;
|
||||
options: CodeLanguage[];
|
||||
onSelectLanguage: (value: AvailableLanguages) => void;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
export const LanguageSelector = ({
|
||||
selectedLanguage,
|
||||
options,
|
||||
onSelectLanguage,
|
||||
showLabel = false,
|
||||
}: LanguageSelectorProps) => {
|
||||
const assetBasePath = useAssetBasePath();
|
||||
const languageOptions = useMemo(
|
||||
|
@ -48,6 +50,16 @@ export const LanguageSelector = ({
|
|||
);
|
||||
return (
|
||||
<EuiSuperSelect<AvailableLanguages>
|
||||
prepend={
|
||||
showLabel
|
||||
? i18n.translate('xpack.searchIndices.codeLanguage.selectLabel', {
|
||||
defaultMessage: 'Language',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
aria-label={i18n.translate('xpack.searchIndices.codeLanguage.selectLabel', {
|
||||
defaultMessage: 'Select a programming language for the code examples',
|
||||
})}
|
||||
options={languageOptions}
|
||||
valueOfSelected={selectedLanguage}
|
||||
onChange={(value) => onSelectLanguage(value)}
|
||||
|
|
|
@ -23,6 +23,8 @@ import { CreateIndexFormState, CreateIndexViewMode } from '../../types';
|
|||
import { CreateIndexPanel } from '../shared/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 {
|
||||
const defaultIndexName = generateRandomIndexName();
|
||||
|
@ -48,6 +50,7 @@ export const ElasticsearchStart: React.FC<ElasticsearchStartProps> = () => {
|
|||
: CreateIndexViewMode.UI
|
||||
);
|
||||
const usageTracker = useUsageTracker();
|
||||
const { workflow, setSelectedWorkflowId } = useWorkflow();
|
||||
|
||||
useEffect(() => {
|
||||
usageTracker.load(AnalyticsEvents.startPageOpened);
|
||||
|
@ -114,6 +117,14 @@ export const ElasticsearchStart: React.FC<ElasticsearchStartProps> = () => {
|
|||
selectedLanguage={formState.codingLanguage}
|
||||
indexName={formState.indexName}
|
||||
changeCodingLanguage={onChangeCodingLanguage}
|
||||
changeWorkflowId={(workflowId: WorkflowId) => {
|
||||
setSelectedWorkflowId(workflowId);
|
||||
usageTracker.click([
|
||||
AnalyticsEvents.startCreateIndexWorkflowSelect,
|
||||
`${AnalyticsEvents.startCreateIndexWorkflowSelect}_${workflowId}`,
|
||||
]);
|
||||
}}
|
||||
selectedWorkflow={workflow}
|
||||
canCreateApiKey={userPrivileges?.privileges.canCreateApiKeys}
|
||||
analyticsEvents={{
|
||||
runInConsole: AnalyticsEvents.startCreateIndexRunInConsole,
|
||||
|
|
|
@ -27,7 +27,7 @@ export const useIndexDocumentSearch = (indexName: string) => {
|
|||
const {
|
||||
services: { http },
|
||||
} = useKibana();
|
||||
const response = useQuery({
|
||||
const { data, isInitialLoading } = useQuery({
|
||||
queryKey: [QueryKeys.SearchDocuments, indexName],
|
||||
refetchInterval: INDEX_SEARCH_POLLING,
|
||||
refetchIntervalInBackground: true,
|
||||
|
@ -46,7 +46,8 @@ export const useIndexDocumentSearch = (indexName: string) => {
|
|||
}),
|
||||
});
|
||||
return {
|
||||
...response,
|
||||
meta: pageToPagination(response?.data?.results?._meta?.page ?? DEFAULT_PAGINATION),
|
||||
data,
|
||||
isInitialLoading,
|
||||
meta: pageToPagination(data?.results?._meta?.page ?? DEFAULT_PAGINATION),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ const POLLING_INTERVAL = 15 * 1000;
|
|||
export const useIndex = (indexName: string) => {
|
||||
const { http } = useKibana().services;
|
||||
const queryKey = [QueryKeys.FetchIndex, indexName];
|
||||
const result = useQuery<Index, { body: { statusCode: number; message: string; error: string } }>({
|
||||
return useQuery<Index, { body: { statusCode: number; message: string; error: string } }>({
|
||||
queryKey,
|
||||
refetchInterval: POLLING_INTERVAL,
|
||||
refetchIntervalInBackground: true,
|
||||
|
@ -25,5 +25,4 @@ export const useIndex = (indexName: string) => {
|
|||
queryFn: () =>
|
||||
http.fetch<Index>(`/internal/index_management/indices/${encodeURIComponent(indexName)}`),
|
||||
});
|
||||
return { queryKey, ...result };
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ const POLLING_INTERVAL = 15 * 1000;
|
|||
export const useIndexMapping = (indexName: string) => {
|
||||
const { http } = useKibana().services;
|
||||
const queryKey = [QueryKeys.FetchMapping, indexName];
|
||||
const result = useQuery<Mappings, { body: { message: string; error: string } }>({
|
||||
return useQuery<Mappings, { body: { message: string; error: string } }>({
|
||||
queryKey,
|
||||
refetchInterval: POLLING_INTERVAL,
|
||||
refetchIntervalInBackground: true,
|
||||
|
@ -22,5 +22,4 @@ export const useIndexMapping = (indexName: string) => {
|
|||
queryFn: () =>
|
||||
http.fetch<Mappings>(`/api/index_management/mapping/${encodeURIComponent(indexName)}`),
|
||||
});
|
||||
return { queryKey, ...result };
|
||||
};
|
||||
|
|
|
@ -233,20 +233,6 @@ export function SearchIndexDetailPageProvider({ getService }: FtrProviderContext
|
|||
);
|
||||
},
|
||||
|
||||
async expectSampleDocumentsWithCustomMappings() {
|
||||
await browser.refresh();
|
||||
await testSubjects.existOrFail('ingestDataCodeExample-code-block');
|
||||
expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain(
|
||||
'Example text 1'
|
||||
);
|
||||
expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain(
|
||||
'Example text 2'
|
||||
);
|
||||
expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain(
|
||||
'Example text 3'
|
||||
);
|
||||
},
|
||||
|
||||
async clickFirstDocumentDeleteAction() {
|
||||
await testSubjects.existOrFail('documentMetadataButton');
|
||||
await testSubjects.click('documentMetadataButton');
|
||||
|
|
|
@ -88,17 +88,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
it('should have basic example texts', async () => {
|
||||
await pageObjects.searchIndexDetailsPage.expectHasSampleDocuments();
|
||||
});
|
||||
|
||||
it('should have other example texts when mapping changed', async () => {
|
||||
await es.indices.putMapping({
|
||||
index: indexNameCodeExample,
|
||||
properties: {
|
||||
text: { type: 'text' },
|
||||
number: { type: 'integer' },
|
||||
},
|
||||
});
|
||||
await pageObjects.searchIndexDetailsPage.expectSampleDocumentsWithCustomMappings();
|
||||
});
|
||||
});
|
||||
|
||||
describe('API key details', () => {
|
||||
|
|
|
@ -221,20 +221,6 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont
|
|||
);
|
||||
},
|
||||
|
||||
async expectSampleDocumentsWithCustomMappings() {
|
||||
await browser.refresh();
|
||||
await testSubjects.existOrFail('ingestDataCodeExample-code-block');
|
||||
expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain(
|
||||
'Example text 1'
|
||||
);
|
||||
expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain(
|
||||
'Example text 2'
|
||||
);
|
||||
expect(await testSubjects.getVisibleText('ingestDataCodeExample-code-block')).to.contain(
|
||||
'Example text 3'
|
||||
);
|
||||
},
|
||||
|
||||
async clickFirstDocumentDeleteAction() {
|
||||
await testSubjects.existOrFail('documentMetadataButton');
|
||||
await testSubjects.click('documentMetadataButton');
|
||||
|
|
|
@ -70,17 +70,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
it('should have basic example texts', async () => {
|
||||
await pageObjects.svlSearchIndexDetailPage.expectHasSampleDocuments();
|
||||
});
|
||||
|
||||
it('should have other example texts when mapping changed', async () => {
|
||||
await es.indices.putMapping({
|
||||
index: indexNameCodeExample,
|
||||
properties: {
|
||||
text: { type: 'text' },
|
||||
number: { type: 'integer' },
|
||||
},
|
||||
});
|
||||
await pageObjects.svlSearchIndexDetailPage.expectSampleDocumentsWithCustomMappings();
|
||||
});
|
||||
});
|
||||
|
||||
describe('API key details', () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue