mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
7ac96504f1
commit
12d193803f
8 changed files with 174 additions and 111 deletions
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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}</>;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue