[Index Management] Render extensions summaries on the index details page (#166754)

## Summary

Fixes https://github.com/elastic/kibana/issues/166103

This PR implements the logic to render summaries added via the extension
service on the new index details page. Currently, only the ILM plugin
registers a summary for an index. The extension service will probably be
refactored when working on
https://github.com/elastic/kibana/issues/165107.
I needed to convert the component `IndexLifecycleSummary` from the class
component to the function component. Otherwise there were errors while
rendering the page and I was not able to check for `null` to not render
an empty card.

### Screenshots
#### When no ILM info or ILM plugin is disabled (no changes to the
Overview tab)
<img width="1029" alt="Screenshot 2023-09-19 at 18 51 14"
src="1f619580-415a-4704-befc-a75a3a37efe6">


#### When there is ILM policy
<img width="1027" alt="Screenshot 2023-09-19 at 18 51 32"
src="05105dbf-e6ca-4a1d-ae53-bd42ec030974">
This commit is contained in:
Yulia Čech 2023-09-21 20:55:00 +02:00 committed by GitHub
parent 7ac96504f1
commit 12d193803f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 174 additions and 111 deletions

View file

@ -17,10 +17,11 @@ import {
addLifecyclePolicyActionExtension,
ilmBannerExtension,
ilmFilterExtension,
ilmSummaryExtension,
} 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';
const { httpSetup } = init();
@ -243,20 +244,26 @@ describe('extend index management', () => {
describe('ilm summary extension', () => {
test('should render null when index has no index lifecycle policy', () => {
const extension = ilmSummaryExtension(indexWithoutLifecyclePolicy, getUrlForApp);
const extension = (
<IndexLifecycleSummary index={indexWithoutLifecyclePolicy} getUrlForApp={getUrlForApp} />
);
const rendered = mountWithIntl(extension);
expect(rendered.isEmptyRender()).toBeTruthy();
});
test('should return extension when index has lifecycle policy', () => {
const extension = ilmSummaryExtension(indexWithLifecyclePolicy, getUrlForApp);
const extension = (
<IndexLifecycleSummary index={indexWithLifecyclePolicy} getUrlForApp={getUrlForApp} />
);
expect(extension).toBeDefined();
const rendered = mountWithIntl(extension);
expect(rendered.render()).toMatchSnapshot();
});
test('should return extension when index has lifecycle error', () => {
const extension = ilmSummaryExtension(indexWithLifecycleError, getUrlForApp);
const extension = (
<IndexLifecycleSummary index={indexWithLifecycleError} getUrlForApp={getUrlForApp} />
);
expect(extension).toBeDefined();
const rendered = mountWithIntl(extension);
expect(rendered.render()).toMatchSnapshot();

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { Component, Fragment } from 'react';
import React, { FunctionComponent, Fragment, useState } from 'react';
import moment from 'moment-timezone';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@ -82,34 +82,20 @@ interface Props {
index: Index;
getUrlForApp: ApplicationStart['getUrlForApp'];
}
interface State {
showStackPopover: boolean;
showPhaseExecutionPopover: boolean;
}
export class IndexLifecycleSummary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
showStackPopover: false,
showPhaseExecutionPopover: false,
};
}
toggleStackPopover = () => {
this.setState({ showStackPopover: !this.state.showStackPopover });
export const IndexLifecycleSummary: FunctionComponent<Props> = ({ index, getUrlForApp }) => {
const [showPhaseExecutionPopover, setShowPhaseExecutionPopover] = useState<boolean>(false);
const { ilm } = index;
const togglePhaseExecutionPopover = () => {
setShowPhaseExecutionPopover(!showPhaseExecutionPopover);
};
closeStackPopover = () => {
this.setState({ showStackPopover: false });
const closePhaseExecutionPopover = () => {
setShowPhaseExecutionPopover(false);
};
togglePhaseExecutionPopover = () => {
this.setState({ showPhaseExecutionPopover: !this.state.showPhaseExecutionPopover });
};
closePhaseExecutionPopover = () => {
this.setState({ showPhaseExecutionPopover: false });
};
renderPhaseExecutionPopoverButton(ilm: IndexLifecyclePolicy) {
const renderPhaseExecutionPopoverButton = () => {
const button = (
<EuiLink onClick={this.togglePhaseExecutionPopover}>
<EuiLink onClick={togglePhaseExecutionPopover}>
<FormattedMessage
defaultMessage="Show definition"
id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.showPhaseDefinitionButton"
@ -131,8 +117,8 @@ export class IndexLifecycleSummary extends Component<Props, State> {
key="phaseExecutionPopover"
id="phaseExecutionPopover"
button={button}
isOpen={this.state.showPhaseExecutionPopover}
closePopover={this.closePhaseExecutionPopover}
isOpen={showPhaseExecutionPopover}
closePopover={closePhaseExecutionPopover}
>
<EuiPopoverTitle>
<FormattedMessage
@ -147,11 +133,8 @@ export class IndexLifecycleSummary extends Component<Props, State> {
</EuiDescriptionListDescription>
</Fragment>
);
}
buildRows() {
const {
index: { ilm },
} = this.props;
};
const buildRows = () => {
const headers = getHeaders();
const rows: {
left: JSX.Element[];
@ -168,7 +151,7 @@ export class IndexLifecycleSummary extends Component<Props, State> {
} else if (fieldName === 'policy') {
content = (
<EuiLink
href={this.props.getUrlForApp('management', {
href={getUrlForApp('management', {
path: `data/index_lifecycle_management/${getPolicyEditPath(value)}`,
})}
>
@ -196,72 +179,67 @@ export class IndexLifecycleSummary extends Component<Props, State> {
}
});
if (ilm.phase_execution) {
rows.right.push(this.renderPhaseExecutionPopoverButton(ilm));
rows.right.push(renderPhaseExecutionPopoverButton());
}
return rows;
}
};
render() {
const {
index: { ilm },
} = this.props;
if (!ilm.managed) {
return null;
}
const { left, right } = this.buildRows();
return (
<>
<EuiTitle size="s">
<h3>
<FormattedMessage
defaultMessage="Index lifecycle management"
id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.summaryTitle"
/>
</h3>
</EuiTitle>
{ilm.step_info && ilm.step_info.type ? (
<>
<EuiSpacer size="s" />
<EuiCallOut
color="danger"
title={
<FormattedMessage
defaultMessage="Index lifecycle error"
id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.summaryErrorMessage"
/>
}
iconType="cross"
>
{ilm.step_info.type}: {ilm.step_info.reason}
</EuiCallOut>
</>
) : null}
{ilm.step_info && ilm.step_info!.message ? (
<>
<EuiSpacer size="s" />
<EuiCallOut
color="primary"
title={
<FormattedMessage
defaultMessage="Action status"
id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.actionStatusTitle"
/>
}
>
{ilm.step_info!.message}
</EuiCallOut>
</>
) : null}
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiDescriptionList type="column">{left}</EuiDescriptionList>
</EuiFlexItem>
<EuiFlexItem>
<EuiDescriptionList type="column">{right}</EuiDescriptionList>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
if (!ilm.managed) {
return null;
}
}
const { left, right } = buildRows();
return (
<>
<EuiTitle size="s">
<h3>
<FormattedMessage
defaultMessage="Index lifecycle management"
id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.summaryTitle"
/>
</h3>
</EuiTitle>
{ilm.step_info && ilm.step_info.type ? (
<>
<EuiSpacer size="s" />
<EuiCallOut
color="danger"
title={
<FormattedMessage
defaultMessage="Index lifecycle error"
id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.summaryErrorMessage"
/>
}
iconType="cross"
>
{ilm.step_info.type}: {ilm.step_info.reason}
</EuiCallOut>
</>
) : null}
{ilm.step_info && ilm.step_info!.message ? (
<>
<EuiSpacer size="s" />
<EuiCallOut
color="primary"
title={
<FormattedMessage
defaultMessage="Action status"
id="xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.actionStatusTitle"
/>
}
>
{ilm.step_info!.message}
</EuiCallOut>
</>
) : null}
<EuiSpacer size="m" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiDescriptionList type="column">{left}</EuiDescriptionList>
</EuiFlexItem>
<EuiFlexItem>
<EuiDescriptionList type="column">{right}</EuiDescriptionList>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -144,13 +144,6 @@ export const ilmBannerExtension = (indices: Index[]) => {
};
};
export const ilmSummaryExtension = (
index: Index,
getUrlForApp: ApplicationStart['getUrlForApp']
) => {
return <IndexLifecycleSummary index={index} getUrlForApp={getUrlForApp} />;
};
export const ilmFilterExtension = (indices: Index[]) => {
const hasIlm = some(indices, (index) => index.ilm && index.ilm.managed);
if (!hasIlm) {
@ -231,6 +224,6 @@ export const addAllExtensions = (
extensionsService.addAction(addLifecyclePolicyActionExtension);
extensionsService.addBanner(ilmBannerExtension);
extensionsService.addSummary(ilmSummaryExtension);
extensionsService.addSummary(IndexLifecycleSummary);
extensionsService.addFilter(ilmFilterExtension);
};

View file

@ -80,6 +80,7 @@ export interface IndexDetailsPageTestBed extends TestBed {
indexStatsContentExists: () => boolean;
indexDetailsContentExists: () => boolean;
addDocCodeBlockExists: () => boolean;
extensionSummaryExists: (index: number) => boolean;
};
};
}
@ -131,6 +132,9 @@ export const setup = async (
addDocCodeBlockExists: () => {
return exists('codeBlockControlsPanel');
},
extensionSummaryExists: (index: number) => {
return exists(`extensionsSummary-${index}`);
},
};
const mappings = {

View file

@ -222,6 +222,51 @@ describe('<IndexDetailsPage />', () => {
expect(testBed.actions.overview.indexDetailsContentExists()).toBe(true);
expect(testBed.actions.overview.indexStatsContentExists()).toBe(false);
});
describe('extension service summary', () => {
it('renders all summaries added to the extension service', async () => {
await act(async () => {
testBed = await setup(httpSetup, {
services: {
extensionsService: {
summaries: [() => <span>test</span>, () => <span>test2</span>],
},
},
});
});
testBed.component.update();
expect(testBed.actions.overview.extensionSummaryExists(0)).toBe(true);
expect(testBed.actions.overview.extensionSummaryExists(1)).toBe(true);
});
it(`doesn't render empty panels if the summary renders null`, async () => {
await act(async () => {
testBed = await setup(httpSetup, {
services: {
extensionsService: {
summaries: [() => null],
},
},
});
});
testBed.component.update();
expect(testBed.actions.overview.extensionSummaryExists(0)).toBe(false);
});
it(`doesn't render anything when no summaries added to the extension service`, async () => {
await act(async () => {
testBed = await setup(httpSetup, {
services: {
extensionsService: {
summaries: [],
},
},
});
});
testBed.component.update();
expect(testBed.actions.overview.extensionSummaryExists(0)).toBe(false);
});
});
});
it('documents tab', async () => {

View file

@ -65,10 +65,11 @@ export class Summary extends React.PureComponent {
const { index } = this.props;
const extensions = extensionsService.summaries;
return extensions.map((summaryExtension, i) => {
const ExtensionSummaryComponent = summaryExtension;
return (
<Fragment key={`summaryExtension-${i}`}>
<EuiHorizontalRule />
{summaryExtension(index, getUrlForApp)}
<ExtensionSummaryComponent index={index} getUrlForApp={getUrlForApp} />
</Fragment>
);
});

View file

@ -28,6 +28,7 @@ import type { Index } from '../../../../../../../common';
import { useAppContext } from '../../../../../app_context';
import { breadcrumbService, IndexManagementBreadcrumb } from '../../../../../services/breadcrumbs';
import { languageDefinitions, curlDefinition } from './languages';
import { ExtensionsSummary } from './extensions_summary';
interface Props {
indexDetails: Index;
@ -157,6 +158,8 @@ export const DetailsPageOverview: React.FunctionComponent<Props> = ({ indexDetai
<EuiSpacer />
<ExtensionsSummary index={indexDetails} />
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiTitle size="s">

View file

@ -0,0 +1,32 @@
/*
* 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, { Fragment, FunctionComponent } from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { Index } from '../../../../../../../common';
import { useAppContext } from '../../../../../app_context';
export const ExtensionsSummary: FunctionComponent<{ index: Index }> = ({ index }) => {
const {
services: { extensionsService },
core: { getUrlForApp },
} = useAppContext();
const summaries = extensionsService.summaries.map((summaryExtension, i) => {
const summary = summaryExtension({ index, getUrlForApp });
if (!summary) {
return null;
}
return (
<Fragment key={`extensionsSummary-${i}`}>
<EuiPanel data-test-subj={`extensionsSummary-${i}`}>{summary}</EuiPanel>
<EuiSpacer />
</Fragment>
);
});
return <>{summaries}</>;
};