[Search][Onboarding] Adding new index details page for search indices (#191313)

## Summary

As part of improving onboarding initiative, adding new page & routes for
index detail page.

**Screenshot** 
<img width="1727" alt="Screenshot 2024-09-05 at 12 08 08 PM"
src="https://github.com/user-attachments/assets/1dc19380-ef53-43f8-93b2-8894114970cd">



**Note to reviewers:**

Features added in this PR
1. added new route -
`/app/elasticsearch/indices/index_details/${indexName}`
2. Back to indices -> redirects to index list page 
3. FTR tests


**How to test:** 
1. Enable searchIndices plugin in `kibana.dev.yml` as this plugin is
behind Feature flag
```
xpack.searchIndices.enabled: true

```
2. Create a new index 
3. Navigate to `/app/elasticsearch/indices/index_details/${indexName}` 

### Checklist

Delete any items that are not applicable to this PR.

- [ ] 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)

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Saarika Bhasi 2024-09-06 12:39:55 -04:00 committed by GitHub
parent 9a187df02d
commit 7fe372ece2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 236 additions and 6 deletions

View file

@ -13,14 +13,15 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { I18nProvider } from '@kbn/i18n-react';
import { QueryClientProvider } from '@tanstack/react-query';
import { Router } from '@kbn/shared-ux-router';
import { UsageTrackerContextProvider } from './contexts/usage_tracker_context';
import { initQueryClient } from './services/query_client';
import { SearchIndicesServicesContext } from './types';
import { SearchIndicesServicesContextDeps } from './types';
export const renderApp = async (
App: React.FC<{}>,
core: CoreStart,
services: Partial<SearchIndicesServicesContext>,
services: SearchIndicesServicesContextDeps,
element: HTMLElement
) => {
const queryClient = initQueryClient(core.notifications.toasts);
@ -30,7 +31,9 @@ export const renderApp = async (
<UsageTrackerContextProvider usageCollection={services.usageCollection}>
<I18nProvider>
<QueryClientProvider client={queryClient}>
<App />
<Router history={services.history}>
<App />
</Router>
</QueryClientProvider>
</I18nProvider>
</UsageTrackerContextProvider>

View file

@ -0,0 +1,59 @@
/*
* 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 { EuiPageSection, EuiSpacer, EuiButton, EuiPageTemplate } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n-react';
import { useIndex } from '../../hooks/api/use_index';
import { useKibana } from '../../hooks/use_kibana';
export const SearchIndexDetailsPage = () => {
const indexName = decodeURIComponent(useParams<{ indexName: string }>().indexName);
const { console: consolePlugin, application } = useKibana().services;
const { data: index } = useIndex(indexName);
const embeddableConsole = useMemo(
() => (consolePlugin?.EmbeddableConsole ? <consolePlugin.EmbeddableConsole /> : null),
[consolePlugin]
);
const navigateToIndexListPage = useCallback(() => {
application.navigateToApp('management', { deepLinkId: 'index_management' });
}, [application]);
return (
<EuiPageTemplate
offset={0}
restrictWidth={false}
data-test-subj="searchIndicesDetailsPage"
grow={false}
bottomBorder={false}
>
<EuiPageSection>
<EuiButton
data-test-subj="searchIndexDetailsBackToIndicesButton"
color="text"
iconType="arrowLeft"
onClick={navigateToIndexListPage}
>
<FormattedMessage
id="xpack.searchIndices.backToIndicesButtonLabel"
defaultMessage="Back to indices"
/>
</EuiButton>
</EuiPageSection>
<EuiPageTemplate.Header
data-test-subj="searchIndexDetailsHeader"
pageTitle={index?.name}
rightSideItems={[]}
/>
<EuiSpacer size="l" />
<div data-test-subj="searchIndexDetailsContent" />
{embeddableConsole}
</EuiPageTemplate>
);
};

View file

@ -0,0 +1,21 @@
/*
* 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 { Route, Routes } from '@kbn/shared-ux-router';
import { SEARCH_INDICES_DETAILS_PATH } from '../../routes';
import { SearchIndexDetailsPage } from './details_page';
import { useKibana } from '../../hooks/use_kibana';
export const SearchIndicesRouter: React.FC = () => {
const { application } = useKibana().services;
return (
<Routes>
<Route exact path={SEARCH_INDICES_DETAILS_PATH} component={SearchIndexDetailsPage} />
<Route render={() => application.navigateToApp('elasticsearchStart')} />
</Routes>
);
};

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { Index } from '@kbn/index-management';
import { useQuery } from '@tanstack/react-query';
import { useKibana } from '../use_kibana';
const POLLING_INTERVAL = 15 * 1000;
export const useIndex = (indexName: string) => {
const { http } = useKibana().services;
const queryKey = ['fetchIndex', indexName];
const result = useQuery({
queryKey,
refetchInterval: POLLING_INTERVAL,
refetchIntervalInBackground: true,
refetchOnWindowFocus: 'always',
retry: true,
queryFn: () =>
http.fetch<Index>(`/internal/index_management/indices/${encodeURIComponent(indexName)}`),
});
return { queryKey, ...result };
};

View file

@ -11,7 +11,7 @@ import type {
SearchIndicesAppPluginStartDependencies,
SearchIndicesPluginSetup,
SearchIndicesPluginStart,
SearchIndicesServicesContext,
SearchIndicesServicesContextDeps,
} from './types';
export class SearchIndicesPlugin
@ -23,20 +23,37 @@ export class SearchIndicesPlugin
core.application.register({
id: 'elasticsearchStart',
appRoute: '/app/elasticsearch/start',
title: i18n.translate('xpack.searchIndices.startAppTitle', {
title: i18n.translate('xpack.searchIndices.elasticsearchStart.startAppTitle', {
defaultMessage: 'Elasticsearch Start',
}),
async mount({ element, history }) {
const { renderApp } = await import('./application');
const { ElasticsearchStartPage } = await import('./components/start/start_page');
const [coreStart, depsStart] = await core.getStartServices();
const startDeps: Partial<SearchIndicesServicesContext> = {
const startDeps: SearchIndicesServicesContextDeps = {
...depsStart,
history,
};
return renderApp(ElasticsearchStartPage, coreStart, startDeps, element);
},
});
core.application.register({
id: 'elasticsearchIndices',
appRoute: '/app/elasticsearch/indices',
title: i18n.translate('xpack.searchIndices.elasticsearchIndices.startAppTitle', {
defaultMessage: 'Elasticsearch Indices',
}),
async mount({ element, history }) {
const { renderApp } = await import('./application');
const { SearchIndicesRouter } = await import('./components/indices/indices_router');
const [coreStart, depsStart] = await core.getStartServices();
const startDeps: SearchIndicesServicesContextDeps = {
...depsStart,
history,
};
return renderApp(SearchIndicesRouter, coreStart, startDeps, element);
},
});
return {
enabled: true,

View file

@ -0,0 +1,9 @@
/*
* 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 ROOT_PATH = '/';
export const SEARCH_INDICES_DETAILS_PATH = `${ROOT_PATH}index_details/:indexName`;

View file

@ -27,6 +27,10 @@ export interface SearchIndicesAppPluginStartDependencies {
usageCollection?: UsageCollectionStart;
}
export interface SearchIndicesServicesContextDeps {
history: AppMountParameters['history'];
usageCollection?: UsageCollectionStart;
}
export type SearchIndicesServicesContext = CoreStart &
SearchIndicesAppPluginStartDependencies & {
history: AppMountParameters['history'];

View file

@ -26,6 +26,8 @@
"@kbn/console-plugin",
"@kbn/share-plugin",
"@kbn/kibana-utils-plugin",
"@kbn/shared-ux-router",
"@kbn/index-management",
],
"exclude": [
"target/**/*",

View file

@ -21,6 +21,7 @@ import { SvlSearchConnectorsPageProvider } from './svl_search_connectors_page';
import { SvlManagementPageProvider } from './svl_management_page';
import { SvlIngestPipelines } from './svl_ingest_pipelines';
import { SvlSearchHomePageProvider } from './svl_search_homepage';
import { SvlSearchIndexDetailPageProvider } from './svl_search_index_detail_page';
export const pageObjects = {
...xpackFunctionalPageObjects,
@ -39,4 +40,5 @@ export const pageObjects = {
svlManagementPage: SvlManagementPageProvider,
svlIngestPipelines: SvlIngestPipelines,
svlSearchHomePage: SvlSearchHomePageProvider,
svlSearchIndexDetailPage: SvlSearchIndexDetailPageProvider,
};

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
const browser = getService('browser');
return {
async expectToBeIndexDetailPage() {
expect(await browser.getCurrentUrl()).contain('/index_details');
},
async expectIndexDetailPageHeader() {
await testSubjects.existOrFail('searchIndexDetailsHeader', { timeout: 2000 });
},
async expectIndexDetailPage() {
await testSubjects.existOrFail('searchIndicesDetailsPage', { timeout: 2000 });
},
async expectBackToIndicesButtonExists() {
await testSubjects.existOrFail('searchIndexDetailsBackToIndicesButton', { timeout: 2000 });
},
async clickBackToIndicesButton() {
await testSubjects.click('searchIndexDetailsBackToIndicesButton');
},
async expectBackToIndicesButtonRedirectsToListPage() {
await testSubjects.existOrFail('indicesList');
},
};
}

View file

@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('serverless search UI - feature flags', function () {
// add tests that require feature flags, defined in config.feature_flags.ts
loadTestFile(require.resolve('./elasticsearch_start.ts'));
loadTestFile(require.resolve('./search_index_detail.ts'));
loadTestFile(require.resolve('../common/platform_security/navigation/management_nav_cards.ts'));
loadTestFile(require.resolve('../common/platform_security/roles.ts'));
loadTestFile(require.resolve('../common/spaces/multiple_spaces_enabled.ts'));

View file

@ -0,0 +1,51 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
import { testHasEmbeddedConsole } from './embedded_console';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const pageObjects = getPageObjects([
'svlCommonPage',
'embeddedConsole',
'svlSearchIndexDetailPage',
]);
const es = getService('es');
const retry = getService('retry');
const PageObjects = getPageObjects(['common']);
const esDeleteAllIndices = getService('esDeleteAllIndices');
const indexName = 'test-my-index';
describe('search index detail page', () => {
before(async () => {
await pageObjects.svlCommonPage.loginWithRole('developer');
await es.indices.create({ index: indexName });
await retry.tryForTime(60 * 1000, async () => {
await PageObjects.common.navigateToApp(`elasticsearch/indices/index_details/${indexName}`, {
shouldLoginIfPrompted: false,
});
await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPage();
});
});
after(async () => {
await esDeleteAllIndices(indexName);
});
it('loads index detail page', async () => {
await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader();
await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPage();
});
it('should have embedded dev console', async () => {
await testHasEmbeddedConsole(pageObjects);
});
it('should redirect to indices list page', async () => {
await pageObjects.svlSearchIndexDetailPage.expectBackToIndicesButtonExists();
await pageObjects.svlSearchIndexDetailPage.clickBackToIndicesButton();
await pageObjects.svlSearchIndexDetailPage.expectBackToIndicesButtonRedirectsToListPage();
});
});
}