Skip to content

Commit

Permalink
[Index Management] Add content to index details page via extensions s…
Browse files Browse the repository at this point in the history
…ervice (#170054)

## Summary

Fixes #168704

This PR adds a function to the extensions service that allows to render
custom content on overview tab of the index details page. When custom
content is set, it will be rendered instead of the code block describing
adding documents to the index. This PR also moves the ILM content from
the overview tab to a separate tab. We will work on the design of this
tab in a follow up PR.

### How to test
To test the custom content apply changes in this
[commit](16769d6).

### Screenshots 

#### Custom content (example)
<img width="1357" alt="Screenshot 2023-11-01 at 19 03 32"
src="https://github.com/elastic/kibana/assets/6585477/71372458-4cc2-413d-bf5f-bb29bff73095">


#### ILM tab
<img width="1129" alt="Screenshot 2023-11-01 at 18 54 07"
src="https://github.com/elastic/kibana/assets/6585477/52c09a73-7d75-4f5f-8d52-b704cd9e6859">



### Checklist

- [ ] 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
- [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
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] 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 renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces&mdash;unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes&mdash;Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](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: Alison Goryachev <alisonmllr20@gmail.com>
  • Loading branch information
3 people authored Nov 3, 2023
1 parent ba4f606 commit fdec4bf
Show file tree
Hide file tree
Showing 25 changed files with 456 additions and 408 deletions.
2 changes: 1 addition & 1 deletion docs/developer/plugin-list.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ Index Management by running this series of requests in Console:
|{kib-repo}blob/{branch}/x-pack/plugins/index_management/README.md[indexManagement]
|Create an index with special characters and verify it renders correctly:
|This service is exposed from the Index Management setup contract and can be used to add content to the indices list and the index details page.
|{kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
* 2.0.
*/

import React from 'react';
import moment from 'moment-timezone';

import { init } from '../integration_tests/helpers/http_requests';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks';
import { Index } from '../common/types';
import {
retryLifecycleActionExtension,
removeLifecyclePolicyActionExtension,
Expand All @@ -20,8 +20,8 @@ import {
} from '../public/extend_index_management';
import { init as initHttp } from '../public/application/services/http';
import { init as initUiMetric } from '../public/application/services/ui_metric';
import { IndexLifecycleSummary } from '../public/extend_index_management/components/index_lifecycle_summary';
import React from 'react';
import { indexLifecycleTab } from '../public/extend_index_management/components/index_lifecycle_summary';
import { Index } from '@kbn/index-management-plugin/common';

const { httpSetup } = init();

Expand Down Expand Up @@ -113,6 +113,7 @@ const indexWithLifecycleError: Index = {
},
phase_execution: {
policy: 'testy',
// @ts-expect-error ILM type is incorrect https://github.com/elastic/elasticsearch-specification/issues/2326
phase_definition: { min_age: '0s', actions: { rollover: { max_size: '1gb' } } },
version: 1,
modified_date_in_millis: 1544031699844,
Expand Down Expand Up @@ -243,29 +244,31 @@ describe('extend index management', () => {
});

describe('ilm summary extension', () => {
test('should render null when index has no index lifecycle policy', () => {
const extension = (
<IndexLifecycleSummary index={indexWithoutLifecyclePolicy} getUrlForApp={getUrlForApp} />
);
const rendered = mountWithIntl(extension);
expect(rendered.isEmptyRender()).toBeTruthy();
const IlmComponent = indexLifecycleTab.renderTabContent;
test('should not render the tab when index has no index lifecycle policy', () => {
const shouldRenderTab =
indexLifecycleTab.shouldRenderTab &&
indexLifecycleTab.shouldRenderTab({
index: indexWithoutLifecyclePolicy,
});
expect(shouldRenderTab).toBeFalsy();
});

test('should return extension when index has lifecycle policy', () => {
const extension = (
<IndexLifecycleSummary index={indexWithLifecyclePolicy} getUrlForApp={getUrlForApp} />
const ilmContent = (
<IlmComponent index={indexWithLifecyclePolicy} getUrlForApp={getUrlForApp} />
);
expect(extension).toBeDefined();
const rendered = mountWithIntl(extension);
expect(ilmContent).toBeDefined();
const rendered = mountWithIntl(ilmContent);
expect(rendered.render()).toMatchSnapshot();
});

test('should return extension when index has lifecycle error', () => {
const extension = (
<IndexLifecycleSummary index={indexWithLifecycleError} getUrlForApp={getUrlForApp} />
const ilmContent = (
<IlmComponent index={indexWithLifecycleError} getUrlForApp={getUrlForApp} />
);
expect(extension).toBeDefined();
const rendered = mountWithIntl(extension);
expect(ilmContent).toBeDefined();
const rendered = mountWithIntl(ilmContent);
expect(rendered.render()).toMatchSnapshot();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
* 2.0.
*/

import { Index as IndexInterface } from '@kbn/index-management-plugin/common/types';

export type Phase = keyof Phases;

export type PhaseWithAllocation = 'warm' | 'cold';
Expand Down Expand Up @@ -244,7 +242,3 @@ export interface IndexLifecyclePolicy {
};
step_time_millis?: number;
}

export interface Index extends IndexInterface {
ilm: IndexLifecyclePolicy;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ import {
EuiModalHeaderTitle,
} from '@elastic/eui';

import { Index } from '@kbn/index-management-plugin/common';
import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api';
import { showApiError } from '../../application/services/api_errors';
import { toasts } from '../../application/services/notification';
import { Index, PolicyFromES } from '../../../common/types';
import { PolicyFromES } from '../../../common/types';

interface Props {
indexName: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ import {
} from '@elastic/eui';

import { ApplicationStart } from '@kbn/core/public';
import { Index } from '@kbn/index-management-plugin/common';
import { IndexDetailsTab } from '@kbn/index-management-plugin/common/constants';
import { IlmExplainLifecycleLifecycleExplainManaged } from '@elastic/elasticsearch/lib/api/types';
import { getPolicyEditPath } from '../../application/services/navigation';
import { Index, IndexLifecyclePolicy } from '../../../common/types';

const getHeaders = (): Array<[keyof IndexLifecyclePolicy, string]> => {
const getHeaders = (): Array<[keyof IlmExplainLifecycleLifecycleExplainManaged, string]> => {
return [
[
'policy',
Expand Down Expand Up @@ -85,7 +87,9 @@ interface Props {

export const IndexLifecycleSummary: FunctionComponent<Props> = ({ index, getUrlForApp }) => {
const [showPhaseExecutionPopover, setShowPhaseExecutionPopover] = useState<boolean>(false);
const { ilm } = index;
const { ilm: ilmData } = index;
// only ILM managed indices render the ILM tab
const ilm = ilmData as IlmExplainLifecycleLifecycleExplainManaged;

const togglePhaseExecutionPopover = () => {
setShowPhaseExecutionPopover(!showPhaseExecutionPopover);
Expand Down Expand Up @@ -144,15 +148,15 @@ export const IndexLifecycleSummary: FunctionComponent<Props> = ({ index, getUrlF
right: [],
};
headers.forEach(([fieldName, label], arrayIndex) => {
const value: any = ilm[fieldName];
const value = ilm[fieldName];
let content;
if (fieldName === 'action_time_millis') {
content = moment(value).format('YYYY-MM-DD HH:mm:ss');
content = moment(value as string).format('YYYY-MM-DD HH:mm:ss');
} else if (fieldName === 'policy') {
content = (
<EuiLink
href={getUrlForApp('management', {
path: `data/index_lifecycle_management/${getPolicyEditPath(value)}`,
path: `data/index_lifecycle_management/${getPolicyEditPath(value as string)}`,
})}
>
{value}
Expand Down Expand Up @@ -184,9 +188,6 @@ export const IndexLifecycleSummary: FunctionComponent<Props> = ({ index, getUrlF
return rows;
};

if (!ilm.managed) {
return null;
}
const { left, right } = buildRows();
return (
<>
Expand Down Expand Up @@ -243,3 +244,18 @@ export const IndexLifecycleSummary: FunctionComponent<Props> = ({ index, getUrlF
</>
);
};

export const indexLifecycleTab: IndexDetailsTab = {
id: 'ilm',
name: (
<FormattedMessage
defaultMessage="Index lifecycle"
id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.tabHeaderLabel"
/>
),
order: 50,
renderTabContent: IndexLifecycleSummary,
shouldRenderTab: ({ index }) => {
return !!index.ilm && index.ilm.managed;
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,19 @@ import { i18n } from '@kbn/i18n';
import { EuiSearchBar } from '@elastic/eui';
import { ApplicationStart } from '@kbn/core/public';

import { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public';
import { Index, IndexManagementPluginSetup } from '@kbn/index-management-plugin/public';

import { retryLifecycleForIndex } from '../application/services/api';
import { IndexLifecycleSummary } from './components/index_lifecycle_summary';
import { indexLifecycleTab } from './components/index_lifecycle_summary';

import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal';
import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle_confirm_modal';
import { Index } from '../../common/types';

const stepPath = 'ilm.step';

export const retryLifecycleActionExtension = ({ indices }: { indices: Index[] }) => {
const allHaveErrors = every(indices, (index) => {
return index.ilm && index.ilm.failed_step;
return index.ilm?.managed && index.ilm.failed_step;
});
if (!allHaveErrors) {
return null;
Expand Down Expand Up @@ -224,6 +223,7 @@ export const addAllExtensions = (
extensionsService.addAction(addLifecyclePolicyActionExtension);

extensionsService.addBanner(ilmBannerExtension);
extensionsService.addSummary(IndexLifecycleSummary);
extensionsService.addFilter(ilmFilterExtension);

extensionsService.addIndexDetailsTab(indexLifecycleTab);
};
8 changes: 3 additions & 5 deletions x-pack/plugins/index_lifecycle_management/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@ import { i18n } from '@kbn/i18n';
import { CoreSetup, Plugin, Logger, PluginInitializerContext } from '@kbn/core/server';
import { IScopedClusterClient } from '@kbn/core/server';

import { Index as IndexWithoutIlm } from '@kbn/index-management-plugin/common/types';
import { Index } from '@kbn/index-management-plugin/common/types';
import { PLUGIN } from '../common/constants';
import { Index } from '../common/types';
import { Dependencies } from './types';
import { registerApiRoutes } from './routes';
import { License } from './services';
import { IndexLifecycleManagementConfig } from './config';
import { handleEsError } from './shared_imports';

const indexLifecycleDataEnricher = async (
indicesList: IndexWithoutIlm[],
indicesList: Index[],
client: IScopedClusterClient
): Promise<Index[]> => {
if (!indicesList || !indicesList.length) {
Expand All @@ -29,8 +28,7 @@ const indexLifecycleDataEnricher = async (
const { indices: ilmIndicesData } = await client.asCurrentUser.ilm.explainLifecycle({
index: '*',
});
// @ts-expect-error IndexLifecyclePolicy is not compatible with IlmExplainLifecycleResponse
return indicesList.map((index: IndexWithoutIlm) => {
return indicesList.map((index: Index) => {
return {
...index,
ilm: { ...(ilmIndicesData[index.name] || {}) },
Expand Down
43 changes: 43 additions & 0 deletions x-pack/plugins/index_management/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,47 @@
# Index Management UI
## Extensions service
This service is exposed from the Index Management setup contract and can be used to add content to the indices list and the index details page.
### Extensions to the indices list
- `addBanner(banner: any)`: adds a banner on top of the indices list, for example when some indices run into an ILM issue
- `addFilter(filter: any)`: adds a filter to the indices list, for example to filter indices managed by ILM
- `addToggle(toggle: any)`: adds a toggle to the indices list, for example to display hidden indices

#### Extensions to the indices list and the index details page
- `addAction(action: any)`: adds an option to the "manage index" menu, for example to add an ILM policy to the index
- `addBadge(badge: any)`: adds a badge to the index name, for example to indicate frozen, rollup or follower indices

#### Extensions to the index details page
- `addIndexDetailsTab(tab: IndexDetailsTab)`: adds a tab to the index details page. The tab has the following interface:

```ts
interface IndexDetailsTab {
// a unique key to identify the tab
id: IndexDetailsTabId;
// a text that is displayed on the tab label, usually a Formatted message component
name: ReactNode;
// a function that renders the content of the tab
renderTabContent: (args: {
index: Index;
getUrlForApp: ApplicationStart['getUrlForApp'];
}) => ReturnType<FunctionComponent>;
// a number to specify the order of the tabs
order: number;
// an optional function to return a boolean for when to render the tab
// if omitted, the tab is always rendered
shouldRenderTab?: (args: { index: Index }) => boolean;
}
```

An example of adding an ILM tab can be found in [this file](https://github.com/elastic/kibana/blob/main/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx#L250).

- `setIndexOverviewContent(content: IndexOverviewContent)`: replaces the default content in the overview tab (code block describing adding documents to the index) with the custom content. The custom content has the following interface:
```ts
interface IndexOverviewContent {
renderContent: (args: {
index: Index;
getUrlForApp: ApplicationStart['getUrlForApp'];
}) => ReturnType<FunctionComponent>;
```
## Indices tab
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@ import { act } from 'react-dom/test-utils';
import { setupEnvironment, nextTick } from '../helpers';
import { HomeTestBed, setup } from './home.helpers';

/**
* The below import is required to avoid a console error warn from the "brace" package
* console.warn ../node_modules/brace/index.js:3999
Could not load worker ReferenceError: Worker is not defined
at createWorker (/<path-to-repo>/node_modules/brace/index.js:17992:5)
*/
import { stubWebWorker } from '@kbn/test-jest-helpers';
stubWebWorker();

describe('<IndexManagementHome />', () => {
const { httpSetup, httpRequestsMockHelpers } = setupEnvironment();
let testBed: HomeTestBed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ describe('<IndexManagementHome />', () => {
it('navigates to the index details page when the index name is clicked', async () => {
const indexName = 'testIndex';
httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]);
httpRequestsMockHelpers.setLoadIndexDetailsResponse(
indexName,
createNonDataStreamIndex(indexName)
);

testBed = await setup(httpSetup, {
history: createMemoryHistory(),
Expand All @@ -150,6 +154,10 @@ describe('<IndexManagementHome />', () => {
it('index page works with % character in index name', async () => {
const indexName = 'test%';
httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]);
httpRequestsMockHelpers.setLoadIndexDetailsResponse(
encodeURIComponent(indexName),
createNonDataStreamIndex(indexName)
);

testBed = await setup(httpSetup);
const { component, actions } = testBed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { HttpSetup } from '@kbn/core/public';
import { act } from 'react-dom/test-utils';

import { IndexDetailsTabIds } from '../../../common/constants';
import { IndexDetailsTabId } from '../../../common/constants';
import { IndexDetailsPage } from '../../../public/application/sections/home/index_list/details_page';
import { WithAppDependencies } from '../helpers';
import { testIndexName } from './mocks';
Expand All @@ -35,7 +35,7 @@ export interface IndexDetailsPageTestBed extends TestBed {
routerMock: typeof reactRouterMock;
actions: {
getHeader: () => string;
clickIndexDetailsTab: (tab: IndexDetailsTabIds) => Promise<void>;
clickIndexDetailsTab: (tab: IndexDetailsTabId) => Promise<void>;
getIndexDetailsTabs: () => string[];
getActiveTabContent: () => string;
mappings: {
Expand Down Expand Up @@ -88,7 +88,6 @@ export interface IndexDetailsPageTestBed extends TestBed {
getDataStreamDetailsContent: () => string;
reloadDataStreamDetails: () => Promise<void>;
addDocCodeBlockExists: () => boolean;
extensionSummaryExists: (index: number) => boolean;
};
};
}
Expand Down Expand Up @@ -127,7 +126,7 @@ export const setup = async ({
return component.find('[data-test-subj="indexDetailsHeader"] h1').text();
};

const clickIndexDetailsTab = async (tab: IndexDetailsTabIds) => {
const clickIndexDetailsTab = async (tab: IndexDetailsTabId) => {
await act(async () => {
find(`indexDetailsTab-${tab}`).simulate('click');
});
Expand Down Expand Up @@ -178,9 +177,6 @@ export const setup = async ({
addDocCodeBlockExists: () => {
return exists('codeBlockControlsPanel');
},
extensionSummaryExists: (index: number) => {
return exists(`extensionsSummary-${index}`);
},
};

const mappings = {
Expand Down
Loading

0 comments on commit fdec4bf

Please sign in to comment.