[Onboarding] [Stack] Add Onboarding experience into Stack (#204351)

## Summary

**TODO**
- [x] FTR - solution navigation ftr - add test for index management
- [x] FTR - fix the index management index list page test to navigate
through the solution navigation to index management list page
- [x] code - playground create index action needs to check if part of es
solution navigation
- [x] Unit - add unit for index management with the change for solution
navigation
- [x] Unit - Fix any failures in index management tests
- [x] Fix FTR tests

These changes are only targeting 9.0.

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [ ] 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/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This was checked for breaking HTTP API changes, and any breaking
changes have been approved by the breaking-change committee. The
`release_note:breaking` label should be applied in these situations.
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] 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: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Yan Savitski <yan.savitski@elastic.co>
This commit is contained in:
Joe McElroy 2025-01-16 00:03:25 +00:00 committed by GitHub
parent f2c0ee8bd7
commit 6ccc8523d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 796 additions and 661 deletions

View file

@ -72,7 +72,6 @@ xpack.cloud.serverless.project_type: search
## Enable the Serverless Search plugin
xpack.serverless.search.enabled: true
xpack.searchIndices.enabled: true
## Set the home route
uiSettings.overrides.defaultRoute: /app/elasticsearch

View file

@ -9,6 +9,8 @@ xpack.securitySolution.enabled: false
xpack.search.notebooks.enabled: false
xpack.searchPlayground.enabled: false
xpack.searchInferenceEndpoints.enabled: false
xpack.searchIndices.enabled: false
## Fine-tune the observability solution feature privileges. Also, refer to `serverless.yml` for the project-agnostic overrides.
xpack.features.overrides:

View file

@ -11,6 +11,7 @@ xpack.search.notebooks.enabled: false
xpack.searchPlayground.enabled: false
xpack.searchInferenceEndpoints.enabled: false
xpack.inventory.enabled: false
xpack.searchIndices.enabled: false
## Fine-tune the security solution feature privileges. Also, refer to `serverless.yml` for the project-agnostic overrides.
xpack.features.overrides:

View file

@ -46,7 +46,7 @@ export type SearchVectorSearch = typeof SEARCH_VECTOR_SEARCH;
export type SearchSemanticSearch = typeof SEARCH_SEMANTIC_SEARCH;
export type SearchAISearch = typeof SEARCH_AI_SEARCH;
export type ContentLinkId = 'searchIndices' | 'connectors' | 'webCrawlers';
export type ContentLinkId = 'connectors' | 'webCrawlers';
export type ApplicationsLinkId = 'searchApplications';

View file

@ -19,6 +19,8 @@ export {
SEARCH_SEMANTIC_SEARCH,
SEARCH_AI_SEARCH,
ES_SEARCH_PLAYGROUND_ID,
SEARCH_INDICES_START,
SEARCH_INDICES,
} from './constants';
export type {

View file

@ -138,6 +138,8 @@ export const applicationUsageSchema = {
enterpriseSearchContent: commonSchema,
searchInferenceEndpoints: commonSchema,
searchPlayground: commonSchema,
elasticsearchIndices: commonSchema,
elasticsearchStart: commonSchema,
enterpriseSearchAnalytics: commonSchema,
enterpriseSearchApplications: commonSchema,
enterpriseSearchAISearch: commonSchema,

View file

@ -2360,6 +2360,268 @@
}
}
},
"elasticsearchIndices": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"elasticsearchStart": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"enterpriseSearchAnalytics": {
"properties": {
"appId": {

View file

@ -17283,19 +17283,8 @@
"xpack.enterpriseSearch.nav.relevanceTitle": "Pertinence",
"xpack.enterpriseSearch.nav.searchApplication.contentTitle": "Contenu",
"xpack.enterpriseSearch.nav.searchApplication.docsExplorerTitle": "Explorateur de documents",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.connectorsConfigurationLabel": "Configuration",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerConfigurationLabel": "Configuration",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerSchedulingLabel": "Planification",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.documentsTitle": "Documents",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.domainManagementLabel": "Gérer les domaines",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.indexMappingsTitle": "Mappings d'index",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.overviewTitle": "Aperçu",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.pipelinesLabel": "Pipelines",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.schedulingTitle": "Planification",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.syncRulesLabel": "Règles de synchronisation",
"xpack.enterpriseSearch.navigation.applicationsSearchApplicationsLinkLabel": "Applications de recherche",
"xpack.enterpriseSearch.navigation.contentConnectorsLinkLabel": "Connecteurs",
"xpack.enterpriseSearch.navigation.contentIndicesLinkLabel": "Index",
"xpack.enterpriseSearch.navigation.contentWebcrawlersLinkLabel": "Robots d'indexation",
"xpack.enterpriseSearch.notFound.action1": "Retour à votre tableau de bord",
"xpack.enterpriseSearch.notFound.action2": "Contacter le support technique",

View file

@ -17143,19 +17143,8 @@
"xpack.enterpriseSearch.nav.homeTitle": "ホーム",
"xpack.enterpriseSearch.nav.searchApplication.contentTitle": "コンテンツ",
"xpack.enterpriseSearch.nav.searchApplication.docsExplorerTitle": "ドキュメントエクスプローラー",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.connectorsConfigurationLabel": "構成",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerConfigurationLabel": "構成",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerSchedulingLabel": "スケジュール",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.documentsTitle": "ドキュメント",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.domainManagementLabel": "ドメインを管理",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.indexMappingsTitle": "インデックスマッピング",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.overviewTitle": "概要",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.pipelinesLabel": "パイプライン",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.schedulingTitle": "スケジュール",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.syncRulesLabel": "同期ルール",
"xpack.enterpriseSearch.navigation.applicationsSearchApplicationsLinkLabel": "検索アプリケーション",
"xpack.enterpriseSearch.navigation.contentConnectorsLinkLabel": "コネクター",
"xpack.enterpriseSearch.navigation.contentIndicesLinkLabel": "インデックス",
"xpack.enterpriseSearch.navigation.contentWebcrawlersLinkLabel": "Webクローラー",
"xpack.enterpriseSearch.notFound.action1": "ダッシュボードに戻す",
"xpack.enterpriseSearch.notFound.action2": "サポートに問い合わせる",

View file

@ -16862,19 +16862,8 @@
"xpack.enterpriseSearch.nav.relevanceTitle": "相关性",
"xpack.enterpriseSearch.nav.searchApplication.contentTitle": "内容",
"xpack.enterpriseSearch.nav.searchApplication.docsExplorerTitle": "文档浏览器",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.connectorsConfigurationLabel": "配置",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerConfigurationLabel": "配置",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerSchedulingLabel": "正在计划",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.documentsTitle": "文档",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.domainManagementLabel": "管理域",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.indexMappingsTitle": "索引映射",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.overviewTitle": "概览",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.pipelinesLabel": "管道",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.schedulingTitle": "正在计划",
"xpack.enterpriseSearch.nav.searchIndicesTitle.nav.syncRulesLabel": "同步规则",
"xpack.enterpriseSearch.navigation.applicationsSearchApplicationsLinkLabel": "搜索应用程序",
"xpack.enterpriseSearch.navigation.contentConnectorsLinkLabel": "连接器",
"xpack.enterpriseSearch.navigation.contentIndicesLinkLabel": "索引",
"xpack.enterpriseSearch.navigation.contentWebcrawlersLinkLabel": "网络爬虫",
"xpack.enterpriseSearch.notFound.action1": "返回到您的仪表板",
"xpack.enterpriseSearch.notFound.action2": "联系支持人员",

View file

@ -21,6 +21,7 @@ import {
applicationServiceMock,
fatalErrorsServiceMock,
httpServiceMock,
chromeServiceMock,
} from '@kbn/core/public/mocks';
import { GlobalFlyout } from '@kbn/es-ui-shared-plugin/public';
@ -70,6 +71,7 @@ const appDependencies = {
executionContext: executionContextServiceMock.createStartContract(),
http: httpServiceMock.createSetupContract(),
application: applicationServiceMock.createStartContract(),
chrome: chromeServiceMock.createStartContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
},
plugins: {
@ -105,6 +107,7 @@ const { Provider: KibanaReactContextProvider } = createKibanaReactContext({
uiSettings: uiSettingsServiceMock.createSetupContract(),
settings: settingsServiceMock.createStartContract(),
theme: themeServiceMock.createStartContract(),
chrome: chromeServiceMock.createStartContract(),
kibanaVersion: {
get: () => kibanaVersion,
},

View file

@ -37,6 +37,7 @@ const urlServiceMock = {
}),
},
};
jest.mock('react-use/lib/useObservable', () => () => jest.fn());
describe('Data Streams - Project level max retention', () => {
const { httpSetup, httpRequestsMockHelpers } = setupEnvironment();

View file

@ -26,6 +26,8 @@ import {
createNonDataStreamIndex,
} from './data_streams_tab.helpers';
jest.mock('react-use/lib/useObservable', () => () => jest.fn());
const nonBreakingSpace = ' ';
const urlServiceMock = {

View file

@ -10,6 +10,8 @@ import { act } from 'react-dom/test-utils';
import { setupEnvironment } from '../helpers';
import { HomeTestBed, setup } from './home.helpers';
jest.mock('react-use/lib/useObservable', () => () => jest.fn());
describe('<IndexManagementHome />', () => {
const { httpSetup, httpRequestsMockHelpers } = setupEnvironment();
let testBed: HomeTestBed;

View file

@ -8,22 +8,9 @@
/*
* Mocking EuiSearchBar because its onChange is not firing during tests
*/
import { EuiSearchBoxProps } from '@elastic/eui/src/components/search_bar/search_box';
import { applicationServiceMock } from '@kbn/core/public/mocks';
jest.mock('@elastic/eui/lib/components/search_bar/search_box', () => {
return {
EuiSearchBox: (props: EuiSearchBoxProps) => (
<input
data-test-subj={props['data-test-subj'] || 'mockSearchBox'}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
props.onSearch(event.target.value);
}}
/>
),
};
});
import React from 'react';
import { EuiSearchBoxProps } from '@elastic/eui/src/components/search_bar/search_box';
import { applicationServiceMock } from '@kbn/core/public/mocks';
import { act } from 'react-dom/test-utils';
import { API_BASE_PATH, Index, INTERNAL_API_BASE_PATH } from '../../../common';
@ -41,6 +28,20 @@ import {
IndexManagementBreadcrumb,
} from '../../../public/application/services/breadcrumbs';
jest.mock('@elastic/eui/lib/components/search_bar/search_box', () => {
return {
EuiSearchBox: (props: EuiSearchBoxProps) => (
<input
data-test-subj={props['data-test-subj'] || 'mockSearchBox'}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
props.onSearch(event.target.value);
}}
/>
),
};
});
jest.mock('react-use/lib/useObservable', () => () => jest.fn());
describe('<IndexManagementHome />', () => {
let testBed: IndicesTestBed;
let httpSetup: ReturnType<typeof setupEnvironment>['httpSetup'];

View file

@ -26,7 +26,11 @@ import { setExtensionsService } from '../../public/application/store/selectors/e
import { ExtensionsService } from '../../public/services';
import { kibanaVersion } from '../client_integration/helpers';
import { notificationServiceMock, executionContextServiceMock } from '@kbn/core/public/mocks';
import {
notificationServiceMock,
executionContextServiceMock,
chromeServiceMock,
} from '@kbn/core/public/mocks';
let store = null;
const indices = [];
@ -44,6 +48,8 @@ const getBaseFakeIndex = (isOpen) => {
};
};
jest.mock('react-use/lib/useObservable', () => () => jest.fn());
for (let i = 0; i < 105; i++) {
indices.push({
...getBaseFakeIndex(true),
@ -159,6 +165,7 @@ describe('index table', () => {
core: {
getUrlForApp: () => {},
executionContext: executionContextServiceMock.createStartContract(),
chrome: chromeServiceMock.createStartContract(),
},
plugins: {},
url: urlServiceMock,

View file

@ -19,6 +19,7 @@ import {
HttpSetup,
IUiSettingsClient,
OverlayStart,
ChromeStart,
} from '@kbn/core/public';
import type { MlPluginStart } from '@kbn/ml-plugin/public';
import type { SharePluginStart } from '@kbn/share-plugin/public';
@ -43,6 +44,7 @@ export interface AppDependencies {
http: HttpSetup;
i18n: I18nStart;
theme: ThemeServiceStart;
chrome: ChromeStart;
};
plugins: {
usageCollection: UsageCollectionSetup;

View file

@ -10,7 +10,9 @@ import { FormattedMessage } from '@kbn/i18n-react';
import type { SharePluginStart } from '@kbn/share-plugin/public';
import { EuiButton } from '@elastic/eui';
import useObservable from 'react-use/lib/useObservable';
import { CreateIndexModal } from './create_index_modal';
import { useAppContext } from '../../../../app_context';
export interface CreateIndexButtonProps {
loadIndices: () => void;
@ -20,9 +22,17 @@ export interface CreateIndexButtonProps {
export const CreateIndexButton = ({ loadIndices, share }: CreateIndexButtonProps) => {
const [createIndexModalOpen, setCreateIndexModalOpen] = useState<boolean>(false);
const createIndexUrl = share?.url.locators.get('SEARCH_CREATE_INDEX')?.useUrl({});
const actionProp = createIndexUrl
? { href: createIndexUrl }
: { onClick: () => setCreateIndexModalOpen(true) };
const {
core: { chrome },
} = useAppContext();
const activeSolutionId = useObservable(chrome.getActiveSolutionNavId$());
const actionProp =
createIndexUrl && activeSolutionId === 'es'
? { href: createIndexUrl }
: { onClick: () => setCreateIndexModalOpen(true) };
return (
<>

View file

@ -16,6 +16,8 @@ import {
SEARCH_VECTOR_SEARCH,
SEARCH_SEMANTIC_SEARCH,
SEARCH_AI_SEARCH,
SEARCH_INDICES,
SEARCH_INDICES_START,
} from '@kbn/deeplinks-search';
import { i18n } from '@kbn/i18n';
@ -30,6 +32,8 @@ export const ENTERPRISE_SEARCH_PRODUCT_NAME = i18n.translate('xpack.enterpriseSe
defaultMessage: 'Enterprise Search',
});
export { SEARCH_INDICES_START, SEARCH_INDICES };
export const ENTERPRISE_SEARCH_OVERVIEW_PLUGIN = {
ID: ENTERPRISE_SEARCH_APP_ID,
NAME: SEARCH_PRODUCT_NAME,

View file

@ -43,7 +43,8 @@
"cloud",
"lens",
"share",
"fleet"
"fleet",
"search_indices"
],
"requiredBundles": ["kibanaReact"]
}

View file

@ -1,181 +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 { setMockValues } from '../../../../__mocks__/kea_logic';
import { mockUseRouteMatch, mockUseParams } from '../../../../__mocks__/react_router';
import { isConnectorIndex, isCrawlerIndex } from '../../../utils/indices';
import { mockIndexNameValues } from '../_mocks_/index_name_logic.mock';
jest.mock('../../../../shared/layout', () => ({
generateNavLink: jest.fn(({ to }) => ({ href: to })),
}));
jest.mock('../../../utils/indices');
import { useIndicesNav } from './indices_nav';
describe('useIndicesNav', () => {
beforeEach(() => {
setMockValues(mockIndexNameValues);
mockUseRouteMatch.mockReturnValue(true);
mockUseParams.mockReturnValue({ indexName: 'index-name' });
});
describe('returns empty', () => {
it('does not return index nav items if not on an index route', () => {
mockUseRouteMatch.mockReturnValueOnce(false);
expect(useIndicesNav()).toBeUndefined();
});
it('does not return index nav items if index name is missing', () => {
mockUseParams.mockReturnValue({ indexName: '' });
expect(useIndicesNav()).toBeUndefined();
});
});
describe('returns an array of EUI side nav items', () => {
const BASE_NAV = [
{
id: 'indexName',
name: 'INDEX-NAME',
'data-test-subj': 'IndexLabel',
href: '/search_indices/index-name',
items: [
{
id: 'overview',
name: 'Overview',
href: '/search_indices/index-name/overview',
'data-test-subj': 'IndexOverviewLink',
},
{
id: 'documents',
name: 'Documents',
href: '/search_indices/index-name/documents',
'data-test-subj': 'IndexDocumentsLink',
},
{
id: 'index_mappings',
name: 'Index mappings',
href: '/search_indices/index-name/index_mappings',
'data-test-subj': 'IndexIndexMappingsLink',
},
],
},
];
it('always returns an index label, overview, documents, index mapping link', () => {
expect(useIndicesNav()).toEqual(BASE_NAV);
});
it('returns pipelines with default navs when hasDefaultIngestPipeline is true', () => {
setMockValues({
...mockIndexNameValues,
productFeatures: { hasDefaultIngestPipeline: true },
});
const pipelineItem = {
id: 'pipelines',
name: 'Pipelines',
href: '/search_indices/index-name/pipelines',
'data-test-subj': 'IndexPipelineLink',
};
const baseNavWithPipelines = BASE_NAV.map((navItem) => ({
...navItem,
items: [...navItem.items, pipelineItem],
}));
expect(useIndicesNav()).toEqual(baseNavWithPipelines);
});
describe('connectors nav', () => {
it('returns connectors nav with default navs when isConnectorIndex is true', () => {
jest.mocked(isConnectorIndex).mockReturnValueOnce(true);
const configuration = {
'data-test-subj': 'IndexConnectorsConfigurationLink',
id: 'connectors_configuration',
name: 'Configuration',
href: '/search_indices/index-name/configuration',
};
const scheduling = {
'data-test-subj': 'IndexSchedulingLink',
id: 'scheduling',
name: 'Scheduling',
href: '/search_indices/index-name/scheduling',
};
const baseNavWithConnectors = BASE_NAV.map((navItem) => ({
...navItem,
items: [...navItem.items, configuration, scheduling],
}));
expect(useIndicesNav()).toEqual(baseNavWithConnectors);
});
it('returns connectors nav with sync rules with default navs when isConnectorIndex is true and hasFilteringFeature is true', () => {
jest.mocked(isConnectorIndex).mockReturnValueOnce(true);
setMockValues({ ...mockIndexNameValues, hasFilteringFeature: true });
const configuration = {
'data-test-subj': 'IndexConnectorsConfigurationLink',
id: 'connectors_configuration',
name: 'Configuration',
href: '/search_indices/index-name/configuration',
};
const syncRules = {
'data-test-subj': 'IndexSyncRulesLink',
id: 'syncRules',
name: 'Sync rules',
href: '/search_indices/index-name/sync_rules',
};
const scheduling = {
'data-test-subj': 'IndexSchedulingLink',
id: 'scheduling',
name: 'Scheduling',
href: '/search_indices/index-name/scheduling',
};
const baseNavWithConnectors = BASE_NAV.map((navItem) => ({
...navItem,
items: [...navItem.items, configuration, syncRules, scheduling],
}));
expect(useIndicesNav()).toEqual(baseNavWithConnectors);
});
});
describe('crawlers nav', () => {
it('returns crawlers nav with default navs when isCrawlerIndex is true', () => {
jest.mocked(isCrawlerIndex).mockReturnValueOnce(true);
const domainManagement = {
'data-test-subj': 'IndexDomainManagementLink',
id: 'domain_management',
name: 'Manage Domains',
href: '/search_indices/index-name/domain_management',
};
const configuration = {
'data-test-subj': 'IndexCrawlerConfigurationLink',
id: 'crawler_configuration',
name: 'Configuration',
href: '/search_indices/index-name/crawler_configuration',
};
const scheduling = {
'data-test-subj': 'IndexCrawlerSchedulingLink',
id: 'crawler_scheduling',
name: 'Scheduling',
href: '/search_indices/index-name/scheduling',
};
const baseNavWithCrawlers = BASE_NAV.map((navItem) => ({
...navItem,
items: [...navItem.items, domainManagement, configuration, scheduling],
}));
expect(useIndicesNav()).toEqual(baseNavWithCrawlers);
});
});
});
});

View file

@ -1,223 +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 { useParams, useRouteMatch } from 'react-router-dom';
import { useValues } from 'kea';
import { EuiSideNavItemType } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { generateEncodedPath } from '../../../../shared/encode_path_params';
import { KibanaLogic } from '../../../../shared/kibana';
import { generateNavLink } from '../../../../shared/layout';
import { SEARCH_INDEX_PATH, SEARCH_INDEX_TAB_PATH } from '../../../routes';
import { isConnectorIndex, isCrawlerIndex } from '../../../utils/indices';
import { IndexViewLogic } from '../index_view_logic';
import { SearchIndexTabId } from '../search_index';
export const useIndicesNav = () => {
const isIndexRoute = !!useRouteMatch(SEARCH_INDEX_PATH);
const { indexName } = useParams<{ indexName: string }>();
const { hasFilteringFeature, index } = useValues(IndexViewLogic);
const {
productFeatures: { hasDefaultIngestPipeline },
} = useValues(KibanaLogic);
if (!indexName || !isIndexRoute) return undefined;
const navItems: Array<EuiSideNavItemType<unknown>> = [
{
'data-test-subj': 'IndexLabel',
id: 'indexName',
name: indexName.toUpperCase(),
...generateNavLink({
to: generateEncodedPath(SEARCH_INDEX_PATH, {
indexName,
}),
}),
items: [
{
'data-test-subj': 'IndexOverviewLink',
id: 'overview',
name: i18n.translate('xpack.enterpriseSearch.nav.searchIndicesTitle.nav.overviewTitle', {
defaultMessage: 'Overview',
}),
...generateNavLink({
to: generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
tabId: SearchIndexTabId.OVERVIEW,
}),
}),
},
{
'data-test-subj': 'IndexDocumentsLink',
id: 'documents',
name: i18n.translate('xpack.enterpriseSearch.nav.searchIndicesTitle.nav.documentsTitle', {
defaultMessage: 'Documents',
}),
...generateNavLink({
to: generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
tabId: SearchIndexTabId.DOCUMENTS,
}),
}),
},
{
'data-test-subj': 'IndexIndexMappingsLink',
id: 'index_mappings',
name: i18n.translate(
'xpack.enterpriseSearch.nav.searchIndicesTitle.nav.indexMappingsTitle',
{
defaultMessage: 'Index mappings',
}
),
...generateNavLink({
to: generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
tabId: SearchIndexTabId.INDEX_MAPPINGS,
}),
}),
},
...(isConnectorIndex(index)
? [
{
'data-test-subj': 'IndexConnectorsConfigurationLink',
id: 'connectors_configuration',
name: i18n.translate(
'xpack.enterpriseSearch.nav.searchIndicesTitle.nav.connectorsConfigurationLabel',
{
defaultMessage: 'Configuration',
}
),
...generateNavLink({
to: generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
tabId: SearchIndexTabId.CONFIGURATION,
}),
}),
},
...(hasFilteringFeature
? [
{
'data-test-subj': 'IndexSyncRulesLink',
id: 'syncRules',
name: i18n.translate(
'xpack.enterpriseSearch.nav.searchIndicesTitle.nav.syncRulesLabel',
{
defaultMessage: 'Sync rules',
}
),
...generateNavLink({
to: generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
tabId: SearchIndexTabId.SYNC_RULES,
}),
}),
},
]
: []),
{
'data-test-subj': 'IndexSchedulingLink',
id: 'scheduling',
name: i18n.translate(
'xpack.enterpriseSearch.nav.searchIndicesTitle.nav.schedulingTitle',
{
defaultMessage: 'Scheduling',
}
),
...generateNavLink({
to: generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
tabId: SearchIndexTabId.SCHEDULING,
}),
}),
},
]
: []),
...(isCrawlerIndex(index)
? [
{
'data-test-subj': 'IndexDomainManagementLink',
id: 'domain_management',
name: i18n.translate(
'xpack.enterpriseSearch.nav.searchIndicesTitle.nav.domainManagementLabel',
{
defaultMessage: 'Manage Domains',
}
),
...generateNavLink({
shouldShowActiveForSubroutes: true,
to: generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
tabId: SearchIndexTabId.DOMAIN_MANAGEMENT,
}),
}),
},
{
'data-test-subj': 'IndexCrawlerConfigurationLink',
id: 'crawler_configuration',
name: i18n.translate(
'xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerConfigurationLabel',
{
defaultMessage: 'Configuration',
}
),
...generateNavLink({
to: generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
tabId: SearchIndexTabId.CRAWLER_CONFIGURATION,
}),
}),
},
{
'data-test-subj': 'IndexCrawlerSchedulingLink',
id: 'crawler_scheduling',
name: i18n.translate(
'xpack.enterpriseSearch.nav.searchIndicesTitle.nav.crawlerSchedulingLabel',
{
defaultMessage: 'Scheduling',
}
),
...generateNavLink({
to: generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
tabId: SearchIndexTabId.SCHEDULING,
}),
}),
},
]
: []),
...(hasDefaultIngestPipeline
? [
{
'data-test-subj': 'IndexPipelineLink',
id: 'pipelines',
name: i18n.translate(
'xpack.enterpriseSearch.nav.searchIndicesTitle.nav.pipelinesLabel',
{
defaultMessage: 'Pipelines',
}
),
...generateNavLink({
to: generateEncodedPath(SEARCH_INDEX_TAB_PATH, {
indexName,
tabId: SearchIndexTabId.PIPELINES,
}),
}),
},
]
: []),
],
},
];
return navItems;
};

View file

@ -1,47 +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 '../../../__mocks__/shallow_useeffect.mock';
import { setMockValues } from '../../../__mocks__/kea_logic';
import React from 'react';
const mockUseIndicesNav = jest.fn().mockReturnValue([]);
jest.mock('react-router-dom', () => ({
useParams: () => ({}),
}));
jest.mock('./indices/indices_nav', () => ({
useIndicesNav: (...args: any[]) => mockUseIndicesNav(...args),
}));
import { shallow } from 'enzyme';
import { SearchIndex } from './search_index';
const mockValues = {
hasFilteringFeature: false,
updateSideNavDefinition: jest.fn(),
};
describe('SearchIndex', () => {
it('updates the side nav dynamic links', async () => {
const updateSideNavDefinition = jest.fn();
setMockValues({ ...mockValues, updateSideNavDefinition });
const indicesItems = [{ foo: 'bar' }];
mockUseIndicesNav.mockReturnValueOnce(indicesItems);
shallow(<SearchIndex />);
expect(updateSideNavDefinition).toHaveBeenCalledWith({
indices: indicesItems,
});
});
});

View file

@ -42,7 +42,6 @@ import { IndexError } from './index_error';
import { SearchIndexIndexMappings } from './index_mappings';
import { IndexNameLogic } from './index_name_logic';
import { IndexViewLogic } from './index_view_logic';
import { useIndicesNav } from './indices/indices_nav';
import { SearchIndexOverview } from './overview';
import { SearchIndexPipelines } from './pipelines/pipelines';
@ -82,8 +81,6 @@ export const SearchIndex: React.FC = () => {
updateSideNavDefinition,
} = useValues(KibanaLogic);
const indicesItems = useIndicesNav();
useEffect(() => {
const subscription = guidedOnboarding?.guidedOnboardingApi
?.isGuideStepActive$('appSearch', 'add_data')
@ -117,11 +114,6 @@ export const SearchIndex: React.FC = () => {
return () => subscription?.unsubscribe();
}, [guidedOnboarding, index?.count]);
useEffect(() => {
// We update the new side nav definition with the selected indices items
updateSideNavDefinition({ indices: indicesItems });
}, [indicesItems, updateSideNavDefinition]);
useEffect(() => {
return () => {
updateSideNavDefinition({ indices: undefined });

View file

@ -49,8 +49,7 @@ export const buildBaseClassicNavItems = (): ClassicNavItem[] => {
{
'data-test-subj': 'searchSideNav-Indices',
deepLink: {
link: 'enterpriseSearchContent:searchIndices',
shouldShowActiveForSubroutes: true,
link: 'management:index_management',
},
id: 'search_indices',
},

View file

@ -9,10 +9,6 @@ jest.mock('./nav_link_helpers', () => ({
generateNavLink: jest.fn(({ to, items }) => ({ href: to, items })),
}));
jest.mock('../../enterprise_search_content/components/search_index/indices/indices_nav', () => ({
useIndicesNav: () => [],
}));
import { setMockValues, mockKibanaValues } from '../../__mocks__/kea_logic';
import { renderHook } from '@testing-library/react';
@ -40,10 +36,10 @@ const baseNavItems = [
items: [
{
'data-test-subj': 'searchSideNav-Indices',
href: '/app/elasticsearch/content/search_indices',
href: '/app/management/data/index_management/',
id: 'search_indices',
items: [],
name: 'Indices',
items: undefined,
name: 'Index Management',
},
{
'data-test-subj': 'searchSideNav-Connectors',
@ -147,9 +143,9 @@ const mockNavLinks = [
url: '/app/elasticsearch/overview',
},
{
id: 'enterpriseSearchContent:searchIndices',
title: 'Indices',
url: '/app/elasticsearch/content/search_indices',
id: 'management:index_management',
title: 'Index Management',
url: '/app/management/data/index_management/',
},
{
id: 'enterpriseSearchContent:connectors',

View file

@ -15,7 +15,6 @@ import { i18n } from '@kbn/i18n';
import { ANALYTICS_PLUGIN, APPLICATIONS_PLUGIN } from '../../../../common/constants';
import { SEARCH_APPLICATIONS_PATH, SearchApplicationViewTabs } from '../../applications/routes';
import { useIndicesNav } from '../../enterprise_search_content/components/search_index/indices/indices_nav';
import { KibanaLogic } from '../kibana';
@ -32,8 +31,6 @@ import { generateNavLink } from './nav_link_helpers';
export const useEnterpriseSearchNav = (alwaysReturn = false) => {
const { isSidebarEnabled, getNavLinks } = useValues(KibanaLogic);
const indicesNavItems = useIndicesNav();
const navItems: Array<EuiSideNavItemTypeEnhanced<unknown>> = useMemo(() => {
const baseNavItems = buildBaseClassicNavItems();
const deepLinks = getNavLinks().reduce((links, link) => {
@ -41,8 +38,8 @@ export const useEnterpriseSearchNav = (alwaysReturn = false) => {
return links;
}, {} as Record<string, ChromeNavLink | undefined>);
return generateSideNavItems(baseNavItems, deepLinks, { search_indices: indicesNavItems });
}, [indicesNavItems]);
return generateSideNavItems(baseNavItems, deepLinks);
}, []);
if (!isSidebarEnabled && !alwaysReturn) return undefined;

View file

@ -18,7 +18,6 @@ import { i18n } from '@kbn/i18n';
import type { AddSolutionNavigationArg } from '@kbn/navigation-plugin/public';
import { SEARCH_APPLICATIONS_PATH } from './applications/applications/routes';
import { SEARCH_INDICES_PATH } from './applications/enterprise_search_content/routes';
export interface DynamicSideNavItems {
collections?: Array<EuiSideNavItemType<unknown>>;
@ -77,7 +76,7 @@ export const getNavigationTreeDefinition = ({
id: 'es',
navigationTree$: dynamicItems$.pipe(
debounceTime(10),
map(({ indices, searchApps, collections }) => {
map(({ searchApps, collections }) => {
const navTree: NavigationTreeDefinition = {
body: [
{
@ -116,27 +115,16 @@ export const getNavigationTreeDefinition = ({
{
children: [
{
breadcrumbStatus:
'hidden' /* management sub-pages set their breadcrumbs themselves */,
getIsActive: ({ pathNameSerialized, prepend }) => {
const someSubItemSelected = indices?.some((index) =>
index.items?.some((item) => item.isSelected)
);
if (someSubItemSelected) return false;
return (
pathNameSerialized ===
prepend(`/app/elasticsearch/content${SEARCH_INDICES_PATH}`)
pathNameSerialized.startsWith(
prepend('/app/management/data/index_management/')
) || pathNameSerialized.startsWith(prepend('/app/elasticsearch/indices'))
);
},
link: 'enterpriseSearchContent:searchIndices',
renderAs: 'item',
...(indices
? {
children: indices.map(euiItemTypeToNodeDefinition),
isCollapsible: false,
renderAs: 'accordion',
}
: {}),
link: 'management:index_management',
},
{ link: 'enterpriseSearchContent:connectors' },
{ link: 'enterpriseSearchContent:webCrawlers' },

View file

@ -57,11 +57,7 @@ import { ClientConfigType, InitialAppData } from '../common/types';
import { hasEnterpriseLicense } from '../common/utils/licensing';
import { SEARCH_APPLICATIONS_PATH } from './applications/applications/routes';
import {
CONNECTORS_PATH,
SEARCH_INDICES_PATH,
CRAWLERS_PATH,
} from './applications/enterprise_search_content/routes';
import { CONNECTORS_PATH, CRAWLERS_PATH } from './applications/enterprise_search_content/routes';
import { docLinks } from './applications/shared/doc_links';
import type { DynamicSideNavItems } from './navigation_tree';
@ -117,13 +113,6 @@ const contentLinks: AppDeepLink[] = [
defaultMessage: 'Connectors',
}),
},
{
id: 'searchIndices',
path: `/${SEARCH_INDICES_PATH}`,
title: i18n.translate('xpack.enterpriseSearch.navigation.contentIndicesLinkLabel', {
defaultMessage: 'Indices',
}),
},
{
id: 'webCrawlers',
path: `/${CRAWLERS_PATH}`,

View file

@ -45,6 +45,8 @@ import {
AI_SEARCH_PLUGIN,
APPLICATIONS_PLUGIN,
SEARCH_PRODUCT_NAME,
SEARCH_INDICES,
SEARCH_INDICES_START,
} from '../common/constants';
import {
@ -163,6 +165,8 @@ export class EnterpriseSearchPlugin implements Plugin<void, void, PluginsSetup,
VECTOR_SEARCH_PLUGIN.ID,
SEMANTIC_SEARCH_PLUGIN.ID,
AI_SEARCH_PLUGIN.ID,
SEARCH_INDICES,
SEARCH_INDICES_START,
];
const isCloud = !!cloud?.cloudId;

View file

@ -6,10 +6,11 @@
*/
import type { SearchIndices, SearchStart } from '@kbn/deeplinks-search/deep_links';
import { SEARCH_INDICES, SEARCH_INDICES_START } from '@kbn/deeplinks-search';
export const PLUGIN_ID = 'searchIndices';
export const PLUGIN_NAME = 'searchIndices';
export const START_APP_ID: SearchStart = 'elasticsearchStart';
export const INDICES_APP_ID: SearchIndices = 'elasticsearchIndices';
export const START_APP_ID: SearchStart = SEARCH_INDICES_START;
export const INDICES_APP_ID: SearchIndices = SEARCH_INDICES;
export type { IndicesStatusResponse, UserStartPrivilegesResponse } from './types';

View file

@ -11,4 +11,5 @@ export const GET_USER_PRIVILEGES_ROUTE = '/internal/search_indices/start_privile
export const POST_CREATE_INDEX_ROUTE = '/internal/search_indices/indices/create';
export const INDEX_DOCUMENT_ROUTE = '/internal/search_indices/{indexName}/documents/{id}';
export const SEARCH_DOCUMENTS_ROUTE = '/internal/search_indices/{indexName}/documents/search';

View file

@ -20,7 +20,8 @@
"cloud",
"console",
"usageCollection",
"serverless"
"serverless",
"searchNavigation"
],
"requiredBundles": [
"kibanaReact",

View file

@ -8,7 +8,7 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiLoadingLogo, EuiPageTemplate } from '@elastic/eui';
import { EuiLoadingLogo } from '@elastic/eui';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { useKibana } from '../../hooks/use_kibana';
@ -24,7 +24,7 @@ const CreateIndexLabel = i18n.translate('xpack.searchIndices.createIndex.docTitl
});
export const CreateIndexPage = () => {
const { console: consolePlugin } = useKibana().services;
const { console: consolePlugin, history, searchNavigation } = useKibana().services;
const {
data: indicesData,
isInitialLoading,
@ -39,11 +39,12 @@ export const CreateIndexPage = () => {
usePageChrome(CreateIndexLabel, [...IndexManagementBreadcrumbs, { text: CreateIndexLabel }]);
return (
<EuiPageTemplate
<KibanaPageTemplate
offset={0}
restrictWidth={false}
data-test-subj="elasticsearchCreateIndexPage"
grow={false}
solutionNav={searchNavigation?.useClassicNavigation(history)}
>
<KibanaPageTemplate.Section alignment="center" restrictWidth={false} grow>
{isInitialLoading && <EuiLoadingLogo />}
@ -53,6 +54,6 @@ export const CreateIndexPage = () => {
)}
</KibanaPageTemplate.Section>
{embeddableConsole}
</EuiPageTemplate>
</KibanaPageTemplate>
);
};

View file

@ -21,6 +21,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { SectionLoading } from '@kbn/es-ui-shared-plugin/public';
import { ApiKeyForm } from '@kbn/search-api-keys-components';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { useNavigateToDiscover } from '../../hooks/use_navigate_to_discover';
import { useIndex } from '../../hooks/api/use_index';
import { useKibana } from '../../hooks/use_kibana';
@ -45,7 +46,14 @@ export const SearchIndexDetailsPage = () => {
const indexName = decodeURIComponent(useParams<{ indexName: string }>().indexName);
const tabId = decodeURIComponent(useParams<{ tabId: string }>().tabId);
const { console: consolePlugin, docLinks, application, history, share } = useKibana().services;
const {
console: consolePlugin,
docLinks,
application,
history,
share,
searchNavigation,
} = useKibana().services;
const {
data: index,
refetch,
@ -189,13 +197,14 @@ export const SearchIndexDetailsPage = () => {
}
return (
<EuiPageTemplate
<KibanaPageTemplate
offset={0}
restrictWidth={false}
data-test-subj="searchIndicesDetailsPage"
grow={false}
panelled
bottomBorder
solutionNav={searchNavigation?.useClassicNavigation(history)}
>
{isIndexError || isMappingsError || !index || !mappings || !indexDocuments ? (
<IndexloadingError
@ -311,6 +320,6 @@ export const SearchIndexDetailsPage = () => {
/>
)}
{embeddableConsole}
</EuiPageTemplate>
</KibanaPageTemplate>
);
};

View file

@ -54,7 +54,7 @@ export const QuickStat: React.FC<BaseQuickStatProps> = ({
const id = useGeneratedHtmlId({
prefix: 'formAccordion',
suffix: title,
suffix: title.replace(/\s/g, '_'),
});
return (

View file

@ -7,6 +7,7 @@
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import type { ConsolePluginSetup, ConsolePluginStart } from '@kbn/console-plugin/public';
import type { SearchNavigationPluginStart } from '@kbn/search-navigation/public';
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
import type {
@ -50,6 +51,7 @@ export interface SearchIndicesAppPluginStartDependencies {
serverless?: ServerlessPluginStart;
usageCollection?: UsageCollectionStart;
indexManagement: IndexManagementPluginStart;
searchNavigation?: SearchNavigationPluginStart;
}
export interface SearchIndicesServicesContextDeps {

View file

@ -9,7 +9,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
import { PluginConfigDescriptor } from '@kbn/core/server';
const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
enabled: schema.boolean({ defaultValue: true }),
});
export type SearchIndicesConfig = TypeOf<typeof configSchema>;

View file

@ -9,9 +9,9 @@ import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import type { IRouter } from '@kbn/core/server';
import type { Logger } from '@kbn/logging';
import { fetchSearchResults } from '@kbn/search-index-documents/lib';
import { DEFAULT_DOCS_PER_PAGE } from '@kbn/search-index-documents/types';
import { fetchSearchResults } from '@kbn/search-index-documents/lib';
import { POST_CREATE_INDEX_ROUTE, SEARCH_DOCUMENTS_ROUTE } from '../../common/routes';
import { CreateIndexRequest } from '../../common/types';
import { createIndex } from '../lib/indices';
@ -64,6 +64,7 @@ export function registerIndicesRoutes(router: IRouter, logger: Logger) {
}
}
);
router.post(
{
path: SEARCH_DOCUMENTS_ROUTE,

View file

@ -39,7 +39,8 @@
"@kbn/deeplinks-search",
"@kbn/core-chrome-browser",
"@kbn/serverless",
"@kbn/utility-types"
"@kbn/utility-types",
"@kbn/search-navigation"
],
"exclude": [
"target/**/*",

View file

@ -56,7 +56,8 @@ describe('CreateIndexButton', () => {
url: {
locators: {
get: jest.fn().mockReturnValue({
getUrl: jest.fn().mockReturnValue('mock-create-index-url'),
getUrl: jest.fn().mockReturnValue('mock-url'),
getRedirectUrl: jest.fn().mockReturnValue('mock-shown-url'),
}),
},
},
@ -72,7 +73,7 @@ describe('CreateIndexButton', () => {
fireEvent.click(createIndexButton);
await waitFor(() => {
expect(navigateToUrl).toHaveBeenCalledWith('mock-create-index-url');
expect(navigateToUrl).toHaveBeenCalledWith('mock-url');
});
});
});

View file

@ -15,27 +15,35 @@ export const CreateIndexButton: React.FC = () => {
const {
services: { application, share },
} = useKibana();
const createIndexLocator = useMemo(
() =>
share.url.locators.get('CREATE_INDEX_LOCATOR_ID') ??
share.url.locators.get('SEARCH_CREATE_INDEX'),
() => share.url.locators.get('SEARCH_CREATE_INDEX'),
[share.url.locators]
);
const handleNavigateToIndex = useCallback(async () => {
const createIndexUrl = await createIndexLocator?.getUrl({});
if (createIndexUrl) {
application?.navigateToUrl(createIndexUrl);
}
}, [application, createIndexLocator]);
const handleCreateIndexClick = useCallback(
async (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
if (!createIndexLocator) {
return;
}
const url = await createIndexLocator.getUrl({});
application?.navigateToUrl(url);
},
[application, createIndexLocator]
);
return createIndexLocator ? (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiButton
color="primary"
iconType="plusInCircle"
fill
onClick={handleNavigateToIndex}
data-test-subj="createIndexButton"
href={createIndexLocator.getRedirectUrl({})}
onClick={handleCreateIndexClick}
>
<FormattedMessage
id="xpack.searchPlayground.createIndexButton"

View file

@ -18,11 +18,6 @@ describe('classicNavigationFactory', function () {
url: '/app/elasticsearch/overview',
title: 'Overview',
},
{
id: 'enterpriseSearchContent:searchIndices',
title: 'Indices',
url: '/app/elasticsearch/content/search_indices',
},
{
id: 'enterpriseSearchContent:connectors',
title: 'Connectors',
@ -110,12 +105,6 @@ describe('classicNavigationFactory', function () {
id: 'searchContent',
name: 'Content',
items: [
{
id: 'searchIndices',
deepLink: {
link: 'enterpriseSearchContent:searchIndices',
},
},
{
id: 'searchConnectors',
deepLink: {
@ -131,13 +120,6 @@ describe('classicNavigationFactory', function () {
{
id: 'searchContent',
items: [
{
href: '/app/elasticsearch/content/search_indices',
id: 'searchIndices',
isSelected: false,
name: 'Indices',
onClick: expect.any(Function),
},
{
href: '/app/elasticsearch/content/connectors',
id: 'searchConnectors',
@ -153,20 +135,20 @@ describe('classicNavigationFactory', function () {
it('returns name if provided over the deeplink title', () => {
const items: ClassicNavItem[] = [
{
id: 'searchIndices',
id: 'searchConnectors',
deepLink: {
link: 'enterpriseSearchContent:searchIndices',
link: 'enterpriseSearchContent:connectors',
},
name: 'Index Management',
name: 'Date connectors',
},
];
const solutionNav = classicNavigationFactory(items, core, history);
expect(solutionNav!.items).toEqual([
{
href: '/app/elasticsearch/content/search_indices',
id: 'searchIndices',
href: '/app/elasticsearch/content/connectors',
id: 'searchConnectors',
isSelected: false,
name: 'Index Management',
name: 'Date connectors',
onClick: expect.any(Function),
},
]);

View file

@ -192,6 +192,12 @@ export default async function ({ readConfigFile }) {
enterpriseSearch: {
pathname: '/app/elasticsearch/overview',
},
elasticsearchStart: {
pathname: '/app/elasticsearch/start',
},
elasticsearchIndices: {
pathname: '/app/elasticsearch/indices',
},
},
suiteTags: {

View file

@ -55,6 +55,7 @@ import { WatcherPageObject } from './watcher_page';
import { SearchProfilerPageProvider } from './search_profiler_page';
import { SearchPlaygroundPageProvider } from './search_playground_page';
import { SearchClassicNavigationProvider } from './search_classic_navigation';
import { SearchStartProvider } from './search_start';
import { SearchApiKeysProvider } from './search_api_keys';
import { SearchIndexDetailPageProvider } from './search_index_details_page';
import { SearchNavigationProvider } from './search_navigation';
@ -99,6 +100,7 @@ export const pageObjects = {
rollup: RollupPageObject,
searchApiKeys: SearchApiKeysProvider,
searchClassicNavigation: SearchClassicNavigationProvider,
searchStart: SearchStartProvider,
searchIndexDetailsPage: SearchIndexDetailPageProvider,
searchNavigation: SearchNavigationProvider,
searchProfiler: SearchProfilerPageProvider,

View file

@ -13,13 +13,25 @@ export function SearchIndexDetailPageProvider({ getService }: FtrProviderContext
const browser = getService('browser');
const retry = getService('retry');
const expectIndexDetailPageHeader = async function () {
await testSubjects.existOrFail('searchIndexDetailsHeader', { timeout: 2000 });
};
const expectSearchIndexDetailsTabsExists = async function () {
await testSubjects.existOrFail('dataTab');
await testSubjects.existOrFail('mappingsTab');
await testSubjects.existOrFail('settingsTab');
};
return {
async expectIndexDetailPageHeader() {
await testSubjects.existOrFail('searchIndexDetailsHeader', { timeout: 2000 });
},
expectIndexDetailPageHeader,
expectSearchIndexDetailsTabsExists,
async expectAPIReferenceDocLinkExists() {
await testSubjects.existOrFail('ApiReferenceDoc', { timeout: 2000 });
},
async expectIndexDetailsPageIsLoaded() {
await expectIndexDetailPageHeader();
await expectSearchIndexDetailsTabsExists();
},
async expectActionItemReplacedWhenHasDocs() {
await testSubjects.missingOrFail('ApiReferenceDoc', { timeout: 2000 });
await testSubjects.existOrFail('useInPlaygroundLink', { timeout: 5000 });
@ -270,11 +282,6 @@ export function SearchIndexDetailPageProvider({ getService }: FtrProviderContext
return (await testSubjects.isDisplayed('searchIndexDetailsHeader')) === true;
});
},
async expectSearchIndexDetailsTabsExists() {
await testSubjects.existOrFail('dataTab');
await testSubjects.existOrFail('mappingsTab');
await testSubjects.existOrFail('settingsTab');
},
async expectBreadcrumbNavigationWithIndexName(indexName: string) {
await testSubjects.existOrFail('euiBreadcrumb');

View file

@ -5,30 +5,21 @@
* 2.0.
*/
import { FtrProviderContext } from '../ftr_provider_context';
import type { FtrProviderContext } from '../ftr_provider_context';
export function SearchNavigationProvider({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const PageObjects = getPageObjects(['common']);
const { common, indexManagement, header } = getPageObjects([
'common',
'indexManagement',
'header',
]);
const testSubjects = getService('testSubjects');
return {
async navigateToLandingPage() {
await retry.tryForTime(60 * 1000, async () => {
await PageObjects.common.navigateToApp('landingPage');
// Wait for the side nav, since the landing page will sometimes redirect to index management now
await testSubjects.existOrFail('svlSearchSideNav', { timeout: 2000 });
});
},
async navigateToGettingStartedPage() {
await retry.tryForTime(60 * 1000, async () => {
await PageObjects.common.navigateToApp('serverlessElasticsearch');
await testSubjects.existOrFail('svlSearchOverviewPage', { timeout: 2000 });
});
},
async navigateToElasticsearchStartPage(expectRedirect: boolean = false) {
await retry.tryForTime(60 * 1000, async () => {
await PageObjects.common.navigateToApp('elasticsearchStart', {
await common.navigateToApp('elasticsearchStart', {
shouldLoginIfPrompted: false,
});
if (!expectRedirect) {
@ -38,16 +29,25 @@ export function SearchNavigationProvider({ getService, getPageObjects }: FtrProv
},
async navigateToIndexDetailPage(indexName: string) {
await retry.tryForTime(60 * 1000, async () => {
await PageObjects.common.navigateToApp(`elasticsearch/indices/index_details/${indexName}`, {
await common.navigateToApp(`elasticsearch/indices/index_details/${indexName}`, {
shouldLoginIfPrompted: false,
});
});
await testSubjects.existOrFail('searchIndicesDetailsPage', { timeout: 2000 });
},
async navigateToInferenceManagementPage(expectRedirect: boolean = false) {
await PageObjects.common.navigateToApp('searchInferenceEndpoints', {
await common.navigateToApp('searchInferenceEndpoints', {
shouldLoginIfPrompted: false,
});
},
async navigateToIndexManagementPage() {
await retry.tryForTime(10 * 1000, async () => {
await common.navigateToApp(`indexManagement`);
await indexManagement.changeTabs('indicesTab');
await header.waitUntilLoadingHasFinished();
await indexManagement.expectToBeOnIndicesManagement();
});
},
};
}

View file

@ -0,0 +1,114 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export function SearchStartProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const browser = getService('browser');
const retry = getService('retry');
return {
async expectToBeOnStartPage() {
expect(await browser.getCurrentUrl()).contain('/app/elasticsearch/start');
await testSubjects.existOrFail('elasticsearchStartPage', { timeout: 2000 });
},
async expectToBeOnIndexDetailsPage() {
await retry.tryForTime(60 * 1000, async () => {
expect(await browser.getCurrentUrl()).contain('/app/elasticsearch/indices/index_details');
});
},
async expectToBeOnIndexListPage() {
await retry.tryForTime(60 * 1000, async () => {
expect(await browser.getCurrentUrl()).contain(
'/app/management/data/index_management/indices'
);
});
},
async expectToBeOnMLFileUploadPage() {
await retry.tryForTime(60 * 1000, async () => {
expect(await browser.getCurrentUrl()).contain('/app/ml/filedatavisualizer');
});
},
async expectIndexNameToExist() {
await testSubjects.existOrFail('indexNameField');
},
async setIndexNameValue(value: string) {
await testSubjects.existOrFail('indexNameField');
await testSubjects.setValue('indexNameField', value);
},
async expectCloseCreateIndexButtonExists() {
await testSubjects.existOrFail('closeCreateIndex');
},
async clickCloseCreateIndexButton() {
await testSubjects.existOrFail('closeCreateIndex');
await testSubjects.click('closeCreateIndex');
},
async expectSkipButtonExists() {
await testSubjects.existOrFail('createIndexSkipBtn');
},
async clickSkipButton() {
await testSubjects.existOrFail('createIndexSkipBtn');
await testSubjects.click('createIndexSkipBtn');
},
async expectCreateIndexButtonToExist() {
await testSubjects.existOrFail('createIndexBtn');
},
async expectCreateIndexButtonToBeEnabled() {
await testSubjects.existOrFail('createIndexBtn');
expect(await testSubjects.isEnabled('createIndexBtn')).equal(true);
},
async expectCreateIndexButtonToBeDisabled() {
await testSubjects.existOrFail('createIndexBtn');
expect(await testSubjects.isEnabled('createIndexBtn')).equal(false);
},
async clickCreateIndexButton() {
await testSubjects.existOrFail('createIndexBtn');
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');
},
async clickFileUploadLink() {
await testSubjects.existOrFail('uploadFileLink');
await testSubjects.click('uploadFileLink');
},
async expectAPIKeyVisibleInCodeBlock(apiKey: string) {
await testSubjects.existOrFail('createIndex-code-block');
await retry.try(async () => {
expect(await testSubjects.getVisibleText('createIndex-code-block')).to.contain(apiKey);
});
},
async expectAPIKeyPreGenerated() {
await testSubjects.existOrFail('apiKeyHasBeenGenerated');
},
async expectAPIKeyNotPreGenerated() {
await testSubjects.existOrFail('apiKeyHasNotBeenGenerated');
},
async expectAPIKeyFormNotAvailable() {
await testSubjects.missingOrFail('apiKeyHasNotBeenGenerated');
await testSubjects.missingOrFail('apiKeyHasBeenGenerated');
},
};
}

View file

@ -12,6 +12,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
describe('Search solution tests', function () {
loadTestFile(require.resolve('./tests/classic_navigation'));
loadTestFile(require.resolve('./tests/solution_navigation'));
loadTestFile(require.resolve('./tests/search_start'));
loadTestFile(require.resolve('./tests/search_index_details'));
});
};

View file

@ -11,7 +11,11 @@ export default function searchSolutionNavigation({
getPageObjects,
getService,
}: FtrProviderContext) {
const { common, searchClassicNavigation } = getPageObjects(['common', 'searchClassicNavigation']);
const { common, searchClassicNavigation, indexManagement } = getPageObjects([
'common',
'searchClassicNavigation',
'indexManagement',
]);
const spaces = getService('spaces');
const browser = getService('browser');
@ -43,7 +47,7 @@ export default function searchSolutionNavigation({
await searchClassicNavigation.expectAllNavItems([
{ id: 'Home', label: 'Home' },
{ id: 'Content', label: 'Content' },
{ id: 'Indices', label: 'Indices' },
{ id: 'Indices', label: 'Index Management' },
{ id: 'Connectors', label: 'Connectors' },
{ id: 'Crawlers', label: 'Web Crawlers' },
{ id: 'Build', label: 'Build' },
@ -64,12 +68,6 @@ export default function searchSolutionNavigation({
await searchClassicNavigation.expectNavItemExists('Home');
// Check Content
// > Indices
await searchClassicNavigation.clickNavItem('Indices');
await searchClassicNavigation.expectNavItemActive('Indices');
await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Content');
await searchClassicNavigation.breadcrumbs.expectBreadcrumbExists('Elasticsearch indices');
// > Connectors
await searchClassicNavigation.clickNavItem('Connectors');
await searchClassicNavigation.expectNavItemActive('Connectors');
@ -130,5 +128,10 @@ export default function searchSolutionNavigation({
await expectNoPageReload();
});
it("should redirect to index management when clicking on 'Indices'", async () => {
await searchClassicNavigation.clickNavItem('Indices');
await indexManagement.expectToBeOnIndicesManagement();
});
});
}

View file

@ -62,7 +62,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await esDeleteAllIndices(indexName);
});
it('can load index detail page', async () => {
await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName);
await pageObjects.searchIndexDetailsPage.expectIndexDetailPageHeader();
await pageObjects.searchIndexDetailsPage.expectSearchIndexDetailsTabsExists();
await pageObjects.searchIndexDetailsPage.expectAPIReferenceDocLinkExists();
@ -275,7 +274,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
before(async () => {
await esDeleteAllIndices(indexName);
await es.indices.create({ index: indexName });
await security.testUser.setRoles(['index_management_user']);
});
beforeEach(async () => {
// Navigate to search solution space

View file

@ -0,0 +1,198 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
import { testHasEmbeddedConsole } from './embedded_console';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const { common, searchApiKeys, searchStart, searchNavigation, embeddedConsole } = getPageObjects([
'searchStart',
'common',
'searchApiKeys',
'searchNavigation',
'embeddedConsole',
]);
const esDeleteAllIndices = getService('esDeleteAllIndices');
const es = getService('es');
const browser = getService('browser');
const retry = getService('retry');
const spaces = getService('spaces');
const deleteAllTestIndices = async () => {
await esDeleteAllIndices(['search-*', 'test-*']);
};
describe('Elasticsearch Start [Onboarding Empty State]', function () {
let cleanUp: () => Promise<unknown>;
let spaceCreated: { id: string } = { id: '' };
before(async () => {
// Navigate to the spaces management page which will log us in Kibana
await common.navigateToUrl('management', 'kibana/spaces', {
shouldUseHashForSubUrl: false,
});
// Create a space with the search solution and navigate to its home page
({ cleanUp, space: spaceCreated } = await spaces.create({
name: 'search-ftr',
solution: 'es',
}));
await browser.navigateTo(spaces.getRootUrl(spaceCreated.id));
});
after(async () => {
// Clean up space created
await cleanUp();
await deleteAllTestIndices();
});
describe('Developer rights', function () {
beforeEach(async () => {
await deleteAllTestIndices();
await searchApiKeys.deleteAPIKeys();
await searchNavigation.navigateToElasticsearchStartPage();
});
it('should have embedded dev console', async () => {
await searchStart.expectToBeOnStartPage();
await testHasEmbeddedConsole({ embeddedConsole });
});
it('should support index creation flow with UI', async () => {
await searchStart.expectToBeOnStartPage();
await searchStart.expectCreateIndexUIView();
await searchStart.expectCreateIndexButtonToBeEnabled();
await searchStart.clickCreateIndexButton();
await searchStart.expectToBeOnIndexDetailsPage();
});
it('should support setting index name', async () => {
await searchStart.expectToBeOnStartPage();
await searchStart.expectIndexNameToExist();
await searchStart.setIndexNameValue('INVALID_INDEX');
await searchStart.expectCreateIndexButtonToBeDisabled();
await searchStart.setIndexNameValue('test-index-name');
await searchStart.expectCreateIndexButtonToBeEnabled();
await searchStart.clickCreateIndexButton();
await searchStart.expectToBeOnIndexDetailsPage();
});
it('should redirect to index details when index is created via API', async () => {
await searchStart.expectToBeOnStartPage();
await es.indices.create({ index: 'test-my-index' });
await searchStart.expectToBeOnIndexDetailsPage();
});
it('should redirect to index list when multiple indices are created via API', async () => {
await searchStart.expectToBeOnStartPage();
await es.indices.create({ index: 'test-my-index-001' });
await es.indices.create({ index: 'test-my-index-002' });
await searchStart.expectToBeOnIndexListPage();
});
it('should redirect to indices list if single index exist on page load', async () => {
await searchNavigation.navigateToElasticsearchStartPage();
await es.indices.create({ index: 'test-my-index-001' });
await searchNavigation.navigateToElasticsearchStartPage(true);
await searchStart.expectToBeOnIndexListPage();
});
it('should support switching between UI and Code Views', async () => {
await searchNavigation.navigateToElasticsearchStartPage();
await searchStart.expectCreateIndexUIView();
await searchStart.clickCodeViewButton();
await searchStart.expectCreateIndexCodeView();
await searchStart.clickUIViewButton();
await searchStart.expectCreateIndexUIView();
});
it('should show the api key in code view', async () => {
await searchStart.expectToBeOnStartPage();
await searchStart.clickCodeViewButton();
// sometimes the API key exists in the cluster and its lost in sessionStorage
// if fails we retry to delete the API key and refresh the browser
await retry.try(
async () => {
await searchApiKeys.expectAPIKeyExists();
},
async () => {
await searchApiKeys.deleteAPIKeys();
await browser.refresh();
await searchStart.clickCodeViewButton();
}
);
await searchApiKeys.expectAPIKeyAvailable();
const apiKeyUI = await searchApiKeys.getAPIKeyFromUI();
const apiKeySession = await searchApiKeys.getAPIKeyFromSessionStorage();
expect(apiKeyUI).to.eql(apiKeySession.encoded);
// check that when browser is refreshed, the api key is still available
await browser.refresh();
await searchStart.clickCodeViewButton();
await searchApiKeys.expectAPIKeyAvailable();
await searchStart.expectAPIKeyPreGenerated();
const refreshBrowserApiKeyUI = await searchApiKeys.getAPIKeyFromUI();
expect(refreshBrowserApiKeyUI).to.eql(apiKeyUI);
// check that when api key is invalidated, a new one is generated
await searchApiKeys.invalidateAPIKey(apiKeySession.id);
await browser.refresh();
await searchStart.clickCodeViewButton();
await searchApiKeys.expectAPIKeyAvailable();
const newApiKeyUI = await searchApiKeys.getAPIKeyFromUI();
expect(newApiKeyUI).to.not.eql(apiKeyUI);
await searchStart.expectAPIKeyVisibleInCodeBlock(newApiKeyUI);
});
it('should explicitly ask to create api key when project already has an apikey', async () => {
await searchApiKeys.clearAPIKeySessionStorage();
await searchApiKeys.createAPIKey();
await searchStart.expectToBeOnStartPage();
await searchStart.clickCodeViewButton();
await searchApiKeys.createApiKeyFromFlyout();
await searchApiKeys.expectAPIKeyAvailable();
});
it('Same API Key should be present on start page and index detail view', async () => {
await searchStart.clickCodeViewButton();
await searchApiKeys.expectAPIKeyAvailable();
const apiKeyUI = await searchApiKeys.getAPIKeyFromUI();
await searchStart.clickUIViewButton();
await searchStart.clickCreateIndexButton();
await searchStart.expectToBeOnIndexDetailsPage();
await searchApiKeys.expectAPIKeyAvailable();
const indexDetailsApiKey = await searchApiKeys.getAPIKeyFromUI();
expect(apiKeyUI).to.eql(indexDetailsApiKey);
});
it('should have file upload link', async () => {
await searchStart.expectToBeOnStartPage();
await searchStart.clickFileUploadLink();
await searchStart.expectToBeOnMLFileUploadPage();
});
it('should have close button', async () => {
await searchStart.expectToBeOnStartPage();
await searchStart.expectCloseCreateIndexButtonExists();
await searchStart.clickCloseCreateIndexButton();
await searchStart.expectToBeOnIndexListPage();
});
it('should have skip button', async () => {
await searchStart.expectToBeOnStartPage();
await searchStart.expectSkipButtonExists();
await searchStart.clickSkipButton();
await searchStart.expectToBeOnIndexListPage();
});
});
});
}

View file

@ -11,7 +11,11 @@ export default function searchSolutionNavigation({
getPageObjects,
getService,
}: FtrProviderContext) {
const { common, solutionNavigation } = getPageObjects(['common', 'solutionNavigation']);
const { common, solutionNavigation, indexManagement } = getPageObjects([
'common',
'indexManagement',
'solutionNavigation',
]);
const spaces = getService('spaces');
const browser = getService('browser');
@ -44,7 +48,7 @@ export default function searchSolutionNavigation({
await solutionNavigation.sidenav.expectLinkExists({ text: 'Dev Tools' });
await solutionNavigation.sidenav.expectLinkExists({ text: 'Discover' });
await solutionNavigation.sidenav.expectLinkExists({ text: 'Dashboards' });
await solutionNavigation.sidenav.expectLinkExists({ text: 'Indices' });
await solutionNavigation.sidenav.expectLinkExists({ text: 'Index Management' });
await solutionNavigation.sidenav.expectLinkExists({ text: 'Connectors' });
await solutionNavigation.sidenav.expectLinkExists({ text: 'Web Crawlers' });
await solutionNavigation.sidenav.expectLinkExists({ text: 'Playground' });
@ -107,15 +111,16 @@ export default function searchSolutionNavigation({
// check the Content
// > Indices section
await solutionNavigation.sidenav.clickLink({
deepLinkId: 'enterpriseSearchContent:searchIndices',
deepLinkId: 'management:index_management',
});
await solutionNavigation.sidenav.expectLinkActive({
deepLinkId: 'enterpriseSearchContent:searchIndices',
deepLinkId: 'management:index_management',
});
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Content' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Indices' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Deployment' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Stack Management' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Index Management' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({
deepLinkId: 'enterpriseSearchContent:searchIndices',
text: 'Indices',
});
// > Connectors
await solutionNavigation.sidenav.clickLink({
@ -236,6 +241,13 @@ export default function searchSolutionNavigation({
await expectNoPageReload();
});
it("should redirect to index management when clicking on 'Indices'", async () => {
await solutionNavigation.sidenav.clickLink({
deepLinkId: 'management:index_management',
});
await indexManagement.expectToBeOnIndicesManagement();
});
it('renders only expected items', async () => {
await solutionNavigation.sidenav.openSection('search_project_nav.otherTools');
await solutionNavigation.sidenav.openSection('project_settings_project_nav');
@ -250,7 +262,7 @@ export default function searchSolutionNavigation({
'discover',
'dashboards',
'content',
'enterpriseSearchContent:searchIndices',
'management:index_management',
'enterpriseSearchContent:connectors',
'enterpriseSearchContent:webCrawlers',
'build',

View file

@ -50,14 +50,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
// check the Content > Indices section
await solutionNavigation.sidenav.clickLink({
deepLinkId: 'enterpriseSearchContent:searchIndices',
deepLinkId: 'management:index_management',
});
await solutionNavigation.sidenav.expectLinkActive({
deepLinkId: 'enterpriseSearchContent:searchIndices',
deepLinkId: 'management:index_management',
});
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Indices' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Deployment' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Stack Management' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Index Management' });
await solutionNavigation.breadcrumbs.expectBreadcrumbExists({
deepLinkId: 'enterpriseSearchContent:searchIndices',
text: 'Indices',
});
// navigate to a different section

View file

@ -88,6 +88,8 @@ export default function catalogueTests({ getService }: FtrProviderContext) {
'enterpriseSearchSemanticSearch',
'enterpriseSearchElasticsearch',
'searchPlayground',
'elasticsearchIndices',
'elasticsearchStart',
'searchInferenceEndpoints',
'searchSynonyms',
'appSearch',