diff --git a/x-pack/plugins/search_indices/public/analytics/constants.ts b/x-pack/plugins/search_indices/public/analytics/constants.ts index 77e177bf780635..bfbe476db7a30e 100644 --- a/x-pack/plugins/search_indices/public/analytics/constants.ts +++ b/x-pack/plugins/search_indices/public/analytics/constants.ts @@ -7,5 +7,7 @@ export enum AnalyticsEvents { startPageOpened = 'start_page_opened', + startPageShowCodeClick = 'start_page_show_code', + startPageShowCreateIndexUIClick = 'start_page_show_create_index_ui', startCreateIndexClick = 'start_create_index', } diff --git a/x-pack/plugins/search_indices/public/assets/curl.svg b/x-pack/plugins/search_indices/public/assets/curl.svg new file mode 100644 index 00000000000000..e922b12283f7d6 --- /dev/null +++ b/x-pack/plugins/search_indices/public/assets/curl.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/x-pack/plugins/search_indices/public/assets/javascript.svg b/x-pack/plugins/search_indices/public/assets/javascript.svg new file mode 100644 index 00000000000000..6d514f5448c509 --- /dev/null +++ b/x-pack/plugins/search_indices/public/assets/javascript.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/x-pack/plugins/search_indices/public/assets/python.svg b/x-pack/plugins/search_indices/public/assets/python.svg new file mode 100644 index 00000000000000..bd8a27810c575d --- /dev/null +++ b/x-pack/plugins/search_indices/public/assets/python.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/search_indices/public/code_examples/create_index.ts b/x-pack/plugins/search_indices/public/code_examples/create_index.ts new file mode 100644 index 00000000000000..ec11283b2185f0 --- /dev/null +++ b/x-pack/plugins/search_indices/public/code_examples/create_index.ts @@ -0,0 +1,27 @@ +/* + * 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 { CurlExamples } from './curl'; +import { JavascriptServerlessExamples } from './javascript'; +import { PythonServerlessExamples } from './python'; +import { ConsoleExamples } from './sense'; + +export const DefaultServerlessCodeExamples: CreateIndexCodeExamples = { + sense: ConsoleExamples.default, + curl: CurlExamples.default, + python: PythonServerlessExamples.default, + javascript: JavascriptServerlessExamples.default, +}; + +export const DenseVectorSeverlessCodeExamples: CreateIndexCodeExamples = { + sense: ConsoleExamples.dense_vector, + curl: CurlExamples.dense_vector, + python: PythonServerlessExamples.dense_vector, + javascript: JavascriptServerlessExamples.dense_vector, +}; diff --git a/x-pack/plugins/search_indices/public/code_examples/curl.ts b/x-pack/plugins/search_indices/public/code_examples/curl.ts new file mode 100644 index 00000000000000..c5ee1d581250c7 --- /dev/null +++ b/x-pack/plugins/search_indices/public/code_examples/curl.ts @@ -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'; +import { API_KEY_PLACEHOLDER, INDEX_PLACEHOLDER } from '../constants'; +import { CodeLanguage, CreateIndexLanguageExamples } from '../types'; + +export const CURL_INFO: CodeLanguage = { + id: 'curl', + title: i18n.translate('xpack.searchIndices.codingLanguages.curl', { defaultMessage: 'cURL' }), + icon: 'curl.svg', + codeBlockLanguage: 'shell', +}; + +export const CurlExamples: CreateIndexLanguageExamples = { + default: { + createIndex: ({ elasticsearchURL, apiKey, indexName }) => `curl PUT '${elasticsearchURL}/${ + indexName ?? INDEX_PLACEHOLDER + }' \ +--header 'Authorization: ApiKey ${apiKey ?? API_KEY_PLACEHOLDER}' \ +--header 'Content-Type: application/json'`, + }, + dense_vector: { + 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":{ + "vector":{ + "type": "dense_vector", + "dims": 3 + }, + "text":{ + "type":"text" + } + } + } +}'`, + }, +}; diff --git a/x-pack/plugins/search_indices/public/code_examples/index.ts b/x-pack/plugins/search_indices/public/code_examples/index.ts new file mode 100644 index 00000000000000..fc4019b45a6da6 --- /dev/null +++ b/x-pack/plugins/search_indices/public/code_examples/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { CURL_INFO } from './curl'; +import { JAVASCRIPT_INFO } from './javascript'; +import { PYTHON_INFO } from './python'; + +export const Languages = { + curl: CURL_INFO, + python: PYTHON_INFO, + javascript: JAVASCRIPT_INFO, +}; + +export const LanguageOptions = Object.values(Languages); + +export type AvailableLanguages = keyof typeof Languages; diff --git a/x-pack/plugins/search_indices/public/code_examples/javascript.ts b/x-pack/plugins/search_indices/public/code_examples/javascript.ts new file mode 100644 index 00000000000000..26b7b5e8111ac3 --- /dev/null +++ b/x-pack/plugins/search_indices/public/code_examples/javascript.ts @@ -0,0 +1,68 @@ +/* + * 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'; +import { API_KEY_PLACEHOLDER, INDEX_PLACEHOLDER } from '../constants'; +import { CodeLanguage, CreateIndexLanguageExamples } from '../types'; + +export const JAVASCRIPT_INFO: CodeLanguage = { + id: 'javascript', + title: i18n.translate('xpack.searchIndices.codingLanguages.javascript', { + defaultMessage: 'Javascript', + }), + icon: 'javascript.svg', + codeBlockLanguage: 'javascript', +}; + +const SERVERLESS_INSTALL_CMD = `npm install @elastic/elasticsearch-serverless`; + +export const JavascriptServerlessExamples: CreateIndexLanguageExamples = { + default: { + installCommand: SERVERLESS_INSTALL_CMD, + createIndex: ({ + elasticsearchURL, + apiKey, + indexName, + }) => `import { Client } from "@elastic/elasticsearch-serverless" + +const client = new Client({ + node: '${elasticsearchURL}', + auth: { + apiKey: "${apiKey ?? API_KEY_PLACEHOLDER}" + } +}); + +client.indices.create({ + index: "${indexName ?? INDEX_PLACEHOLDER}", +});`, + }, + dense_vector: { + installCommand: SERVERLESS_INSTALL_CMD, + createIndex: ({ + elasticsearchURL, + apiKey, + indexName, + }) => `import { Client } from "@elastic/elasticsearch-serverless" + +const client = new Client({ + node: '${elasticsearchURL}', + auth: { + apiKey: "${apiKey ?? API_KEY_PLACEHOLDER}" + } +}); + +client.indices.create({ + index: "${indexName ?? INDEX_PLACEHOLDER}", + mappings: { + properties: { + vector: { type: "dense_vector", dims: 3 }, + text: { type: "text"} + }, + }, +});`, + }, +}; diff --git a/x-pack/plugins/search_indices/public/code_examples/python.ts b/x-pack/plugins/search_indices/public/code_examples/python.ts new file mode 100644 index 00000000000000..0d9e778ca1060a --- /dev/null +++ b/x-pack/plugins/search_indices/public/code_examples/python.ts @@ -0,0 +1,62 @@ +/* + * 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'; +import { API_KEY_PLACEHOLDER, INDEX_PLACEHOLDER } from '../constants'; +import { CodeLanguage, CodeSnippetParameters, CreateIndexLanguageExamples } from '../types'; + +export const PYTHON_INFO: CodeLanguage = { + id: 'python', + title: i18n.translate('xpack.searchIndices.codingLanguages.python', { defaultMessage: 'Python' }), + icon: 'python.svg', + codeBlockLanguage: 'python', +}; + +const SERVERLESS_PYTHON_INSTALL_CMD = 'pip install elasticsearch-serverless'; + +export const PythonServerlessExamples: CreateIndexLanguageExamples = { + default: { + installCommand: SERVERLESS_PYTHON_INSTALL_CMD, + createIndex: ({ + elasticsearchURL, + apiKey, + indexName, + }: CodeSnippetParameters) => `from elasticsearch-serverless import Elasticsearch + +client = Elasticsearch( + "${elasticsearchURL}", + api_key="${apiKey ?? API_KEY_PLACEHOLDER}" +) + +client.indices.create( + index="${indexName ?? INDEX_PLACEHOLDER}" +)`, + }, + dense_vector: { + installCommand: SERVERLESS_PYTHON_INSTALL_CMD, + createIndex: ({ + elasticsearchURL, + apiKey, + indexName, + }: CodeSnippetParameters) => `from elasticsearch-serverless import Elasticsearch + +client = Elasticsearch( + "${elasticsearchURL}", + api_key="${apiKey ?? API_KEY_PLACEHOLDER}" +) + +client.indices.create( + index="${indexName ?? INDEX_PLACEHOLDER}" + mappings={ + "properties": { + "vector": {"type": "dense_vector", "dims": 3 }, + "text": {"type": "text"} + } + } +)`, + }, +}; diff --git a/x-pack/plugins/search_indices/public/code_examples/sense.ts b/x-pack/plugins/search_indices/public/code_examples/sense.ts new file mode 100644 index 00000000000000..ad7b5834c9d260 --- /dev/null +++ b/x-pack/plugins/search_indices/public/code_examples/sense.ts @@ -0,0 +1,31 @@ +/* + * 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 { INDEX_PLACEHOLDER } from '../constants'; +import { CreateIndexLanguageExamples } from '../types'; + +export const ConsoleExamples: CreateIndexLanguageExamples = { + default: { + createIndex: ({ indexName }) => `PUT /${indexName ?? INDEX_PLACEHOLDER}`, + }, + dense_vector: { + createIndex: ({ indexName }) => `PUT /${indexName ?? INDEX_PLACEHOLDER} +{ + "mappings": { + "properties":{ + "vector":{ + "type": "dense_vector", + "dims": 3 + }, + "text":{ + "type":"text" + } + } + } +}`, + }, +}; diff --git a/x-pack/plugins/search_indices/public/components/shared/language_selector.tsx b/x-pack/plugins/search_indices/public/components/shared/language_selector.tsx new file mode 100644 index 00000000000000..8d6717bb90541a --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/shared/language_selector.tsx @@ -0,0 +1,56 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiIcon, EuiSuperSelect, EuiText, EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useAssetBasePath } from '../../hooks/use_asset_base_path'; +import { AvailableLanguages } from '../../code_examples'; +import { CodeLanguage } from '../../types'; + +export interface LanguageSelectorProps { + selectedLanguage: AvailableLanguages; + options: CodeLanguage[]; + onSelectLanguage: (value: AvailableLanguages) => void; +} + +export const LanguageSelector = ({ + selectedLanguage, + options, + onSelectLanguage, +}: LanguageSelectorProps) => { + const assetBasePath = useAssetBasePath(); + const languageOptions = useMemo( + () => + options.map((lang) => ({ + value: lang.id as AvailableLanguages, + 'aria-label': i18n.translate('xpack.searchIndices.codeLanguage.selectChangeAriaLabel', { + defaultMessage: + 'Change language of code examples to {language} for every instance on this page', + values: { + language: lang.title, + }, + }), + 'data-test-subj': `lang-option-${lang.id}`, + inputDisplay: ( + + + {lang.title} + + ), + })), + [assetBasePath, options] + ); + return ( + + options={languageOptions} + valueOfSelected={selectedLanguage} + onChange={(value) => onSelectLanguage(value)} + /> + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/start/code_sample.tsx b/x-pack/plugins/search_indices/public/components/start/code_sample.tsx new file mode 100644 index 00000000000000..017424e287a617 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/start/code_sample.tsx @@ -0,0 +1,46 @@ +/* + * 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 from 'react'; +import { + EuiCodeBlock, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiText, + EuiThemeProvider, +} from '@elastic/eui'; + +export interface CodeSampleProps { + title: string; + language: string; + code: string; +} + +export const CodeSample = ({ title, language, code }: CodeSampleProps) => { + return ( + + + {title} + + + + + + {code} + + + + + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/start/create_index.tsx b/x-pack/plugins/search_indices/public/components/start/create_index.tsx index 8bbee0168f7301..ae191481e5da40 100644 --- a/x-pack/plugins/search_indices/public/components/start/create_index.tsx +++ b/x-pack/plugins/search_indices/public/components/start/create_index.tsx @@ -14,34 +14,39 @@ import { EuiForm, EuiFormRow, EuiIcon, + EuiSpacer, EuiText, - EuiTitle, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { UserStartPrivilegesResponse } from '../../../common'; import { AnalyticsEvents } from '../../analytics/constants'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; -import { isValidIndexName, generateRandomIndexName } from '../../utils/indices'; +import { isValidIndexName } from '../../utils/indices'; import { useCreateIndex } from './hooks/use_create_index'; -interface CreateIndexFormState { - indexName: string; -} +import { CreateIndexFormState } from './types'; -function initCreateIndexState(): CreateIndexFormState { - return { - indexName: generateRandomIndexName(), - }; -} +const CREATE_INDEX_CONTENT = i18n.translate( + 'xpack.searchIndices.startPage.createIndex.action.text', + { + defaultMessage: 'Create my index', + } +); export interface CreateIndexFormProps { + formState: CreateIndexFormState; + setFormState: React.Dispatch>; userPrivileges?: UserStartPrivilegesResponse; } -export const CreateIndexForm = ({ userPrivileges }: CreateIndexFormProps) => { - const [formState, setFormState] = useState(initCreateIndexState()); +export const CreateIndexForm = ({ + userPrivileges, + formState, + setFormState, +}: CreateIndexFormProps) => { const [indexNameHasError, setIndexNameHasError] = useState(false); const usageTracker = useUsageTracker(); const { createIndex, isLoading } = useCreateIndex(); @@ -62,95 +67,90 @@ export const CreateIndexForm = ({ userPrivileges }: CreateIndexFormProps) => { }; return ( - - - - - -

- {i18n.translate('xpack.searchIndices.startPage.createIndex.title', { - defaultMessage: 'Create your first index', - })} -

-
-
- - <> - -
- -

- {i18n.translate('xpack.searchIndices.startPage.createIndex.description', { - defaultMessage: - 'An index stores your data and defines the schema, or field mappings, for your searches', - })} -

-
- + + - + + + + + {userPrivileges?.privileges?.canCreateIndex === false ? ( + + {i18n.translate('xpack.searchIndices.startPage.createIndex.permissionTooltip', { + defaultMessage: 'You do not have permission to create an index.', + })} +

} - )} - /> -
- - + > + + {CREATE_INDEX_CONTENT} + + + ) : ( - {i18n.translate('xpack.searchIndices.startPage.createIndex.action.text', { - defaultMessage: 'Create my index', - })} + {CREATE_INDEX_CONTENT} - - - {userPrivileges?.privileges?.canCreateApiKeys && ( - - - -

- {i18n.translate( - 'xpack.searchIndices.startPage.createIndex.apiKeyCreation.description', - { - defaultMessage: "We'll create an API key for this index", - } - )} -

-
-
- )} -
-
+ )} + + + {userPrivileges?.privileges?.canCreateApiKeys && ( + + + +

+ {i18n.translate( + 'xpack.searchIndices.startPage.createIndex.apiKeyCreation.description', + { + defaultMessage: "We'll create an API key for this index", + } + )} +

+
+
+ )} +
); diff --git a/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx b/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx new file mode 100644 index 00000000000000..b58bf6c0926f1c --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/start/create_index_code.tsx @@ -0,0 +1,80 @@ +/* + * 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, { useMemo, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { TryInConsoleButton } from '@kbn/try-in-console'; + +import { useKibana } from '../../hooks/use_kibana'; +import { CodeSample } from './code_sample'; +import { CreateIndexFormState } from './types'; +import { ELASTICSEARCH_URL_PLACEHOLDER } from '../../constants'; + +import { Languages, AvailableLanguages, LanguageOptions } from '../../code_examples'; +import { DenseVectorSeverlessCodeExamples } from '../../code_examples/create_index'; + +import { LanguageSelector } from '../shared/language_selector'; + +export interface CreateIndexCodeViewProps { + createIndexForm: CreateIndexFormState; +} + +// TODO: this will be dynamic based on stack / es3 & onboarding token +const SelectedCodeExamples = DenseVectorSeverlessCodeExamples; + +export const CreateIndexCodeView = ({ createIndexForm }: CreateIndexCodeViewProps) => { + const { application, cloud, share, console: consolePlugin } = useKibana().services; + // TODO: initing this should be dynamic and possibly saved in the form state + const [selectedLanguage, setSelectedLanguage] = useState('python'); + const codeParams = useMemo(() => { + return { + indexName: createIndexForm.indexName || undefined, + elasticsearchURL: cloud?.elasticsearchUrl ?? ELASTICSEARCH_URL_PLACEHOLDER, + }; + }, [createIndexForm.indexName, cloud]); + const selectedCodeExample = useMemo(() => { + return SelectedCodeExamples[selectedLanguage]; + }, [selectedLanguage]); + + return ( + + + + setSelectedLanguage(value)} + /> + + + + + + {selectedCodeExample.installCommand && ( + + )} + + + ); +}; diff --git a/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx b/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx index 18d0bb90b1f0f7..131948d1a03777 100644 --- a/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx +++ b/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx @@ -5,8 +5,18 @@ * 2.0. */ -import React, { useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import React, { useCallback, useEffect, useState } from 'react'; +import { + EuiButtonGroup, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { IndicesStatusResponse, UserStartPrivilegesResponse } from '../../../common'; @@ -14,20 +24,59 @@ import type { IndicesStatusResponse, UserStartPrivilegesResponse } from '../../. import { AnalyticsEvents } from '../../analytics/constants'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; +import { generateRandomIndexName } from '../../utils/indices'; import { CreateIndexForm } from './create_index'; +import { CreateIndexCodeView } from './create_index_code'; +import { CreateIndexFormState } from './types'; -const MAX_WIDTH = '600px'; +function initCreateIndexState(): CreateIndexFormState { + return { + indexName: generateRandomIndexName(), + }; +} + +const MAX_WIDTH = '650px'; +enum CreateIndexView { + UI = 'ui', + Code = 'code', +} export interface ElasticsearchStartProps { indicesData?: IndicesStatusResponse; userPrivileges?: UserStartPrivilegesResponse; } export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) => { + const [createIndexView, setCreateIndexView] = useState( + userPrivileges?.privileges.canCreateIndex === false ? CreateIndexView.Code : CreateIndexView.UI + ); + const [formState, setFormState] = useState(initCreateIndexState()); const usageTracker = useUsageTracker(); useEffect(() => { usageTracker.load(AnalyticsEvents.startPageOpened); }, [usageTracker]); + useEffect(() => { + if (userPrivileges === undefined) return; + if (userPrivileges.privileges.canCreateIndex === false) { + setCreateIndexView(CreateIndexView.Code); + } + }, [userPrivileges]); + const onChangeView = useCallback( + (id) => { + switch (id) { + case CreateIndexView.UI: + usageTracker.click(AnalyticsEvents.startPageShowCreateIndexUIClick); + setCreateIndexView(CreateIndexView.UI); + return; + case CreateIndexView.Code: + usageTracker.click(AnalyticsEvents.startPageShowCodeClick); + setCreateIndexView(CreateIndexView.Code); + return; + } + }, + [usageTracker] + ); + return ( - + + + + + +

+ {i18n.translate('xpack.searchIndices.startPage.createIndex.title', { + defaultMessage: 'Create your first index', + })} +

+
+
+ + + +
+ +

+ {i18n.translate('xpack.searchIndices.startPage.createIndex.description', { + defaultMessage: + 'An index stores your data and defines the schema, or field mappings, for your searches', + })} +

+
+ {createIndexView === CreateIndexView.UI && ( + + )} + {createIndexView === CreateIndexView.Code && ( + + )} +
+
); diff --git a/x-pack/plugins/search_indices/public/components/start/start_page.tsx b/x-pack/plugins/search_indices/public/components/start/start_page.tsx index f26d78302e636f..4a848f580d22fd 100644 --- a/x-pack/plugins/search_indices/public/components/start/start_page.tsx +++ b/x-pack/plugins/search_indices/public/components/start/start_page.tsx @@ -44,7 +44,9 @@ export const ElasticsearchStartPage = () => { {isInitialLoading && } {hasIndicesStatusFetchError && } - + {!isInitialLoading && !hasIndicesStatusFetchError && ( + + )} {embeddableConsole} diff --git a/x-pack/plugins/search_indices/public/components/start/types.ts b/x-pack/plugins/search_indices/public/components/start/types.ts new file mode 100644 index 00000000000000..6b6c1c8e38f61a --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/start/types.ts @@ -0,0 +1,10 @@ +/* + * 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 interface CreateIndexFormState { + indexName: string; +} diff --git a/x-pack/plugins/search_indices/public/constants.ts b/x-pack/plugins/search_indices/public/constants.ts new file mode 100644 index 00000000000000..3e352aa9b0d2f4 --- /dev/null +++ b/x-pack/plugins/search_indices/public/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 ELASTICSEARCH_URL_PLACEHOLDER = 'https://your_deployment_url'; +export const API_KEY_PLACEHOLDER = 'YOUR_API_KEY'; +export const INDEX_PLACEHOLDER = 'my-index'; diff --git a/x-pack/plugins/search_indices/public/hooks/use_asset_base_path.ts b/x-pack/plugins/search_indices/public/hooks/use_asset_base_path.ts new file mode 100644 index 00000000000000..e3c60417cd5218 --- /dev/null +++ b/x-pack/plugins/search_indices/public/hooks/use_asset_base_path.ts @@ -0,0 +1,14 @@ +/* + * 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 { PLUGIN_ID } from '../../common'; +import { useKibana } from './use_kibana'; + +export const useAssetBasePath = () => { + const { http } = useKibana().services; + return http.basePath.prepend(`/plugins/${PLUGIN_ID}/assets`); +}; diff --git a/x-pack/plugins/search_indices/public/types.ts b/x-pack/plugins/search_indices/public/types.ts index fba8e9fb8fbd55..8e7853543f76f7 100644 --- a/x-pack/plugins/search_indices/public/types.ts +++ b/x-pack/plugins/search_indices/public/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { CloudStart } from '@kbn/cloud-plugin/public'; import type { ConsolePluginStart } from '@kbn/console-plugin/public'; import type { AppMountParameters, CoreStart } from '@kbn/core/public'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; @@ -23,6 +24,7 @@ export interface AppPluginStartDependencies { export interface SearchIndicesAppPluginStartDependencies { console?: ConsolePluginStart; + cloud?: CloudStart; share: SharePluginStart; usageCollection?: UsageCollectionStart; } @@ -41,3 +43,34 @@ export interface AppUsageTracker { count: (eventName: string | string[]) => void; load: (eventName: string | string[]) => void; } + +export interface CodeSnippetParameters { + indexName?: string; + apiKey?: string; + elasticsearchURL: string; +} +export type CodeSnippetFunction = (params: CodeSnippetParameters) => string; + +export interface CodeLanguage { + id: string; + title: string; + icon: string; + codeBlockLanguage: string; +} + +export interface CreateIndexCodeDefinition { + installCommand?: string; + createIndex: CodeSnippetFunction; +} + +export interface CreateIndexCodeExamples { + sense: CreateIndexCodeDefinition; + curl: CreateIndexCodeDefinition; + python: CreateIndexCodeDefinition; + javascript: CreateIndexCodeDefinition; +} + +export interface CreateIndexLanguageExamples { + default: CreateIndexCodeDefinition; + dense_vector: CreateIndexCodeDefinition; +} diff --git a/x-pack/plugins/search_indices/tsconfig.json b/x-pack/plugins/search_indices/tsconfig.json index 849fecad1b2979..c8f2397b39a557 100644 --- a/x-pack/plugins/search_indices/tsconfig.json +++ b/x-pack/plugins/search_indices/tsconfig.json @@ -28,6 +28,8 @@ "@kbn/kibana-utils-plugin", "@kbn/shared-ux-router", "@kbn/index-management", + "@kbn/try-in-console", + "@kbn/cloud-plugin", ], "exclude": [ "target/**/*", 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 cb09613d169729..64852a7da8943e 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 @@ -25,6 +25,13 @@ export function SvlSearchElasticsearchStartPageProvider({ getService }: FtrProvi ); }); }, + async expectToBeOnIndexListPage() { + await retry.tryForTime(60 * 1000, async () => { + expect(await browser.getCurrentUrl()).contain( + '/app/management/data/index_management/indices' + ); + }); + }, async expectIndexNameToExist() { await testSubjects.existOrFail('indexNameField'); }, @@ -48,5 +55,19 @@ export function SvlSearchElasticsearchStartPageProvider({ getService }: FtrProvi expect(await testSubjects.isEnabled('createIndexBtn')).equal(true); await testSubjects.click('createIndexBtn'); }, + async expectCreateIndexCodeView() { + await testSubjects.existOrFail('createIndexCodeView'); + }, + async expectCreateIndexUIView() { + await testSubjects.existOrFail('createIndexUIView'); + }, + async clickUIViewButton() { + await testSubjects.existOrFail('createIndexUIViewBtn'); + await testSubjects.click('createIndexUIViewBtn'); + }, + async clickCodeViewButton() { + await testSubjects.existOrFail('createIndexCodeViewBtn'); + await testSubjects.click('createIndexCodeViewBtn'); + }, }; } diff --git a/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts b/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts index ac32a49f3694fb..55f1551141e471 100644 --- a/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts +++ b/x-pack/test_serverless/functional/test_suites/search/elasticsearch_start.ts @@ -43,6 +43,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should support index creation flow with UI', async () => { await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexUIView(); await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexButtonToBeEnabled(); await pageObjects.svlSearchElasticsearchStartPage.clickCreateIndexButton(); await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage(); @@ -64,6 +65,22 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await es.indices.create({ index: 'test-my-index' }); await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage(); }); + + it('should redirect to index list when multiple indices are created via API', async () => { + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); + await es.indices.create({ index: 'test-my-index-001' }); + await es.indices.create({ index: 'test-my-index-002' }); + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexListPage(); + }); + + it('should support switching between UI and Code Views', async () => { + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexUIView(); + await pageObjects.svlSearchElasticsearchStartPage.clickCodeViewButton(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView(); + await pageObjects.svlSearchElasticsearchStartPage.clickUIViewButton(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexUIView(); + }); }); describe('viewer', function () { before(async () => { @@ -77,9 +94,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await deleteAllTestIndices(); }); - it('should redirect to index details when index is created via API', async () => { + it('should default to code view when lacking create index permissions', async () => { await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView(); + await pageObjects.svlSearchElasticsearchStartPage.clickUIViewButton(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexUIView(); await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexButtonToBeDisabled(); + }); + + it('should redirect to index details when index is created via API', async () => { + await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnStartPage(); + await pageObjects.svlSearchElasticsearchStartPage.expectCreateIndexCodeView(); await es.indices.create({ index: 'test-my-api-index' }); await pageObjects.svlSearchElasticsearchStartPage.expectToBeOnIndexDetailsPage(); });