[Onboarding] Expose settings component in index_management to reuse in search_indices (#193492)

## Summary
This PR exposes `index_management` index details [settings component
](https://github.com/elastic/kibana/blob/main/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings.tsx#L16)during
`index_management` plugin start. This would enable `search_indices`
plugin to reuse.

With this change, in new search index details page user can :

- Can View settings 
- Update settings & save 
- Reset changes

<img width="1719" alt="Screenshot 2024-09-19 at 5 48 28 PM"
src="https://github.com/user-attachments/assets/a6179fb6-c180-434e-bdb1-3c784006069f">

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

```
2. [Create new
index](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html)
3. Navigate to
`/app/elasticsearch/indices/index_details/${indexName}/settings`

 

### Checklist


- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Saarika Bhasi 2024-09-24 09:13:31 -04:00 committed by GitHub
parent 92f1320019
commit d925391b90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 220 additions and 28 deletions

View file

@ -25,6 +25,9 @@ export interface IndexManagementPluginStart {
getIndexMappingComponent: (deps: {
history: ScopedHistory<unknown>;
}) => React.FC<IndexMappingProps>;
getIndexSettingsComponent: (deps: {
history: ScopedHistory<unknown>;
}) => React.FC<IndexSettingProps>;
}
export interface Index {
@ -56,7 +59,9 @@ export interface IndexMappingProps {
index?: Index;
showAboutMappings?: boolean;
}
export interface IndexSettingProps {
indexName: string;
}
export interface SendRequestResponse<D = any, E = any> {
data: D | null;
error: E | null;

View file

@ -6,14 +6,14 @@
*/
import React from 'react';
import { documentationService } from '../../../../services';
import { UIM_APP_NAME } from '../../../../../../common/constants/ui_metric';
import { httpService } from '../../../../services/http';
import { notificationService } from '../../../../services/notification';
import { UiMetricService } from '../../../../services/ui_metric';
import { AppDependencies, IndexManagementAppContext } from '../../../..';
import { documentationService } from '../../../../../services';
import { UIM_APP_NAME } from '../../../../../../../common/constants/ui_metric';
import { httpService } from '../../../../../services/http';
import { notificationService } from '../../../../../services/notification';
import { UiMetricService } from '../../../../../services/ui_metric';
import { AppDependencies, IndexManagementAppContext } from '../../../../..';
import { IndexMappingWithContextProps } from './index_mapping_with_context_types';
import { DetailsPageMappings } from './details_page_mappings';
import { DetailsPageMappings } from '../details_page_mappings';
export const IndexMappingWithContext: React.FC<IndexMappingWithContextProps> = ({
core,

View file

@ -7,8 +7,8 @@
import { CoreStart } from '@kbn/core/public';
import { IndexMappingProps } from '@kbn/index-management-shared-types';
import { AppDependencies } from '../../../../app_context';
import { ExtensionsService } from '../../../../../services/extensions_service';
import { AppDependencies } from '../../../../../app_context';
import { ExtensionsService } from '../../../../../../services/extensions_service';
export type IndexMappingWithContextProps = {
core: CoreStart;

View file

@ -0,0 +1,29 @@
/*
* 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.
*/
/* 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 { EuiLoadingSpinner } from '@elastic/eui';
import { dynamic } from '@kbn/shared-ux-utility';
import React, { Suspense, ComponentType } from 'react';
import { IndexSettingWithContextProps } from './index_settings_with_context_types';
const IndexSettingsWithContext = dynamic<ComponentType<IndexSettingWithContextProps>>(() =>
import('./index_settings_with_context').then((mod) => ({ default: mod.IndexSettingsWithContext }))
);
export const IndexSettings: React.FC<IndexSettingWithContextProps> = (props) => {
return (
<Suspense fallback={<EuiLoadingSpinner />}>
<IndexSettingsWithContext {...props} />
</Suspense>
);
};

View file

@ -0,0 +1,52 @@
/*
* 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 { documentationService } from '../../../../../services';
import { UIM_APP_NAME } from '../../../../../../../common/constants/ui_metric';
import { httpService } from '../../../../../services/http';
import { notificationService } from '../../../../../services/notification';
import { UiMetricService } from '../../../../../services/ui_metric';
import { AppDependencies, IndexManagementAppContext } from '../../../../..';
import { DetailsPageSettings } from '../details_page_settings';
import { IndexSettingWithContextProps } from './index_settings_with_context_types';
import { setUiMetricService } from '../../../../../services/api';
export const IndexSettingsWithContext: React.FC<IndexSettingWithContextProps> = ({
core,
dependencies,
indexName,
usageCollection,
}) => {
// this normally happens when the index management app is rendered
// but if components are embedded elsewhere that setup is skipped, so we have to do it here
// would do it in plugin.ts but that blows up the bundle size
// can't do it in an effect because then the first http call fails as the instantiation happens after first render
if (!httpService.httpClient) {
httpService.setup(core.http);
notificationService.setup(core.notifications);
}
documentationService.setup(core.docLinks);
const uiMetricService = new UiMetricService(UIM_APP_NAME);
setUiMetricService(uiMetricService);
uiMetricService.setup(usageCollection);
const newDependencies: AppDependencies = {
...dependencies,
services: {
...(dependencies.services || {}),
httpService,
notificationService,
uiMetricService,
},
};
return (
<IndexManagementAppContext core={core} dependencies={newDependencies}>
<DetailsPageSettings indexName={indexName} />
</IndexManagementAppContext>
);
};

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CoreStart } from '@kbn/core/public';
import type { IndexSettingProps } from '@kbn/index-management-shared-types';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { AppDependencies } from '../../../../../app_context';
import { ExtensionsService } from '../../../../../../services/extensions_service';
export type IndexSettingWithContextProps = {
core: CoreStart;
// omitting services here to constitute them inside the component
// this helps reduce bundle size significantly
dependencies: Omit<AppDependencies, 'services'> & {
services: { extensionsService: ExtensionsService };
};
usageCollection: UsageCollectionSetup;
} & IndexSettingProps;

View file

@ -26,8 +26,9 @@ import { ClientConfigType, SetupDependencies, StartDependencies } from './types'
// avoid import from index files in plugin.ts, use specific import paths
import { PLUGIN } from '../common/constants/plugin';
import { IndexMapping } from './application/sections/home/index_list/details_page/index_mappings_embeddable';
import { IndexMapping } from './application/sections/home/index_list/details_page/with_context_components/index_mappings_embeddable';
import { PublicApiService } from './services/public_api_service';
import { IndexSettings } from './application/sections/home/index_list/details_page/with_context_components/index_settings_embeddable';
export class IndexMgmtUIPlugin
implements
@ -159,6 +160,44 @@ export class IndexMgmtUIPlugin
return IndexMapping({ dependencies: appDependencies, core: coreStart, ...props });
};
},
getIndexSettingsComponent: (deps: { history: ScopedHistory<unknown> }) => {
const { docLinks, fatalErrors, application, uiSettings, executionContext, settings, http } =
coreStart;
const { url } = share;
const appDependencies = {
core: {
fatalErrors,
getUrlForApp: application.getUrlForApp,
executionContext,
application,
http,
},
plugins: {
usageCollection,
isFleetEnabled: Boolean(fleet),
share,
cloud,
console,
ml,
licensing,
},
services: {
extensionsService: this.extensionsService,
},
config: this.config,
history: deps.history,
setBreadcrumbs: undefined as any, // breadcrumbService.setBreadcrumbs,
uiSettings,
settings,
url,
docLinks,
kibanaVersion: this.kibanaVersion,
theme$: coreStart.theme.theme$,
};
return (props: any) => {
return IndexSettings({ dependencies: appDependencies, core: coreStart, ...props });
};
},
};
}
public stop() {}

View file

@ -34,8 +34,9 @@ import { useIndexMapping } from '../../hooks/api/use_index_mappings';
import { IndexDocuments } from '../index_documents/index_documents';
import { DeleteIndexModal } from './delete_index_modal';
import { IndexloadingError } from './details_page_loading_error';
import { SearchIndicesDetailsMappingsTabs } from '../../routes';
import { SearchIndexDetailsTabs } from '../../routes';
import { SearchIndexDetailsMappings } from './details_page_mappings';
import { SearchIndexDetailsSettings } from './details_page_settings';
export const SearchIndexDetailsPage = () => {
const indexName = decodeURIComponent(useParams<{ indexName: string }>().indexName);
@ -49,33 +50,40 @@ export const SearchIndexDetailsPage = () => {
isInitialLoading: isMappingsInitialLoading,
} = useIndexMapping(indexName);
const SearchIndexDetailsTabs: EuiTabbedContentTab[] = useMemo(() => {
const detailsPageTabs: EuiTabbedContentTab[] = useMemo(() => {
return [
{
id: SearchIndicesDetailsMappingsTabs.DATA,
id: SearchIndexDetailsTabs.DATA,
name: i18n.translate('xpack.searchIndices.documentsTabLabel', {
defaultMessage: 'Data',
}),
content: <IndexDocuments indexName={indexName} />,
'data-test-subj': `${SearchIndicesDetailsMappingsTabs.DATA}Tab`,
'data-test-subj': `${SearchIndexDetailsTabs.DATA}Tab`,
},
{
id: SearchIndicesDetailsMappingsTabs.MAPPINGS,
id: SearchIndexDetailsTabs.MAPPINGS,
name: i18n.translate('xpack.searchIndices.mappingsTabLabel', {
defaultMessage: 'Mappings',
}),
content: <SearchIndexDetailsMappings index={index} />,
'data-test-subj': `${SearchIndicesDetailsMappingsTabs.MAPPINGS}Tab`,
'data-test-subj': `${SearchIndexDetailsTabs.MAPPINGS}Tab`,
},
{
id: SearchIndexDetailsTabs.SETTINGS,
name: i18n.translate('xpack.searchIndices.settingsTabLabel', {
defaultMessage: 'Settings',
}),
content: <SearchIndexDetailsSettings indexName={indexName} />,
'data-test-subj': `${SearchIndexDetailsTabs.SETTINGS}Tab`,
},
];
}, [index, indexName]);
const [selectedTab, setSelectedTab] = useState(SearchIndexDetailsTabs[0]);
const [selectedTab, setSelectedTab] = useState(detailsPageTabs[0]);
useEffect(() => {
const newTab = SearchIndexDetailsTabs.find((tab) => tab.id === tabId);
const newTab = detailsPageTabs.find((tab) => tab.id === tabId);
if (newTab) setSelectedTab(newTab);
}, [SearchIndexDetailsTabs, tabId]);
}, [detailsPageTabs, tabId]);
const handleTabClick = useCallback(
(tab) => {
@ -215,7 +223,7 @@ export const SearchIndexDetailsPage = () => {
<EuiFlexItem>
<EuiFlexItem>
<EuiTabbedContent
tabs={SearchIndexDetailsTabs}
tabs={detailsPageTabs}
onTabClick={handleTabClick}
selectedTab={selectedTab}
/>

View file

@ -0,0 +1,24 @@
/*
* 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 { useMemo } from 'react';
import { useKibana } from '../../hooks/use_kibana';
interface SearchIndexDetailsSettingsProps {
indexName: string;
}
export const SearchIndexDetailsSettings = ({ indexName }: SearchIndexDetailsSettingsProps) => {
const { indexManagement, history } = useKibana().services;
const IndexSettingsComponent = useMemo(
() => indexManagement.getIndexSettingsComponent({ history }),
[indexManagement, history]
);
return <IndexSettingsComponent indexName={indexName} />;
};

View file

@ -9,7 +9,7 @@ import { Route, Router, Routes } from '@kbn/shared-ux-router';
import { Redirect } from 'react-router-dom';
import { useKibana } from '../../hooks/use_kibana';
import {
SearchIndicesDetailsMappingsTabs,
SearchIndexDetailsTabs,
SEARCH_INDICES_DETAILS_PATH,
SEARCH_INDICES_DETAILS_TABS_PATH,
} from '../../routes';
@ -25,7 +25,7 @@ export const SearchIndicesRouter: React.FC = () => {
<Redirect
exact
from={`${SEARCH_INDICES_DETAILS_PATH}/`}
to={`${SEARCH_INDICES_DETAILS_PATH}/${SearchIndicesDetailsMappingsTabs.DATA}`}
to={`${SEARCH_INDICES_DETAILS_PATH}/${SearchIndexDetailsTabs.DATA}`}
/>
</Routes>
</Route>

View file

@ -8,7 +8,8 @@
export const ROOT_PATH = '/';
export const SEARCH_INDICES_DETAILS_PATH = `${ROOT_PATH}index_details/:indexName`;
export const SEARCH_INDICES_DETAILS_TABS_PATH = `${SEARCH_INDICES_DETAILS_PATH}/:tabId`;
export enum SearchIndicesDetailsMappingsTabs {
export enum SearchIndexDetailsTabs {
DATA = 'data',
MAPPINGS = 'mappings',
SETTINGS = 'settings',
}

View file

@ -115,11 +115,17 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont
async expectShouldDefaultToDataTab() {
expect(await browser.getCurrentUrl()).contain('/data');
},
async withDataChangeTabs(tab: 'dataTab' | 'mappingsTab') {
async withDataChangeTabs(tab: 'dataTab' | 'mappingsTab' | 'settingsTab') {
await testSubjects.click(tab);
},
async expectUrlShouldChangeTo(tab: 'data' | 'mappings') {
async expectUrlShouldChangeTo(tab: 'data' | 'mappings' | 'settings') {
expect(await browser.getCurrentUrl()).contain(`/${tab}`);
},
async expectMappingsComponentIsVisible() {
await testSubjects.existOrFail('indexDetailsMappingsToggleViewButton', { timeout: 2000 });
},
async expectSettingsComponentIsVisible() {
await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 });
},
};
}

View file

@ -91,9 +91,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await pageObjects.svlSearchIndexDetailPage.expectWithDataTabsExists();
await pageObjects.svlSearchIndexDetailPage.expectShouldDefaultToDataTab();
});
it('should be able to change tabs', async () => {
it('should be able to change tabs to mappings and mappings is shown', async () => {
await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('mappingsTab');
await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('mappings');
await pageObjects.svlSearchIndexDetailPage.expectMappingsComponentIsVisible();
});
it('should be able to change tabs to settings and settings is shown', async () => {
await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('settingsTab');
await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings');
await pageObjects.svlSearchIndexDetailPage.expectSettingsComponentIsVisible();
});
});