+ {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();
});