[ILM] Convert node details flyout to TS (#73707)

* [ILM] Convert node allocation component to TS and use hooks

* [ILM] Fix jest tests

* [ILM] Fix i18n check

* [ILM] Implement code review suggestions

* [ILM] Fix type check, docs link and button maxWidth in NodeAllocation component

* Fix internaliation error

* [ILM] Convert node details flyout to TS

* [ILM] Fix useState declaration

* [ILM] Fix useState declaration

* [ILM] Fix jest test

* [ILM] Change error message when unable to load node attributes

* [ILM] Change error message when unable to load node details

* [ILM] Delete a period in error callout

* [ILM] Delete a period in error callout

* [ILM] Convert node details flyout to TS

* [ILM] Fix i18n check

* [ILM] Fix useState declaration

* [ILM] Fix useState declaration

* [ILM] Fix jest test

* [ILM] Change error message when unable to load node details

* [ILM] Delete a period in error callout

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Yulia Čech 2020-08-11 11:32:05 +02:00 committed by GitHub
parent 92289b63ef
commit a4ec4332c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 149 additions and 193 deletions

View file

@ -253,6 +253,9 @@ describe('edit policy', () => {
beforeEach(() => {
server.respondImmediately = true;
httpRequestsMockHelpers.setNodesListResponse({});
httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [
{ nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } },
]);
});
test('should show number required error when trying to save empty warm phase', async () => {
@ -395,7 +398,9 @@ describe('edit policy', () => {
rendered.update();
const flyoutButton = findTestSubject(rendered, 'warm-viewNodeDetailsFlyoutButton');
expect(flyoutButton.exists()).toBeTruthy();
flyoutButton.simulate('click');
await act(async () => {
await flyoutButton.simulate('click');
});
rendered.update();
expect(rendered.find('.euiFlyout').exists()).toBeTruthy();
});
@ -404,6 +409,9 @@ describe('edit policy', () => {
beforeEach(() => {
server.respondImmediately = true;
httpRequestsMockHelpers.setNodesListResponse({});
httpRequestsMockHelpers.setNodesDetailsResponse('attribute:true', [
{ nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } },
]);
});
test('should allow 0 for phase timing', async () => {
const rendered = mountWithIntl(component);
@ -470,7 +478,9 @@ describe('edit policy', () => {
rendered.update();
const flyoutButton = findTestSubject(rendered, 'cold-viewNodeDetailsFlyoutButton');
expect(flyoutButton.exists()).toBeTruthy();
flyoutButton.simulate('click');
await act(async () => {
await flyoutButton.simulate('click');
});
rendered.update();
expect(rendered.find('.euiFlyout').exists()).toBeTruthy();
});

View file

@ -25,9 +25,18 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
]);
};
const setNodesDetailsResponse = (nodeAttributes: string, response: HttpResponse = []) => {
server.respondWith(`/api/index_lifecycle_management/nodes/${nodeAttributes}/details`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(response),
]);
};
return {
setPoliciesResponse,
setNodesListResponse,
setNodesDetailsResponse,
};
};

View file

@ -34,7 +34,6 @@ import { SetPriorityInput } from '../set_priority_input';
export class ColdPhase extends PureComponent {
static propTypes = {
setPhaseData: PropTypes.func.isRequired,
showNodeDetailsFlyout: PropTypes.func.isRequired,
isShowingErrors: PropTypes.bool.isRequired,
errors: PropTypes.object.isRequired,
@ -42,7 +41,6 @@ export class ColdPhase extends PureComponent {
render() {
const {
setPhaseData,
showNodeDetailsFlyout,
phaseData,
errors,
isShowingErrors,
@ -114,7 +112,6 @@ export class ColdPhase extends PureComponent {
<NodeAllocation
phase={PHASE_COLD}
setPhaseData={setPhaseData}
showNodeDetailsFlyout={showNodeDetailsFlyout}
errors={errors}
phaseData={phaseData}
isShowingErrors={isShowingErrors}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import React, { Fragment, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
@ -20,11 +20,11 @@ import { PHASE_NODE_ATTRS } from '../../../../constants';
import { LearnMoreLink } from '../../../components/learn_more_link';
import { ErrableFormRow } from '../../form_errors';
import { useLoadNodes } from '../../../../services/api';
import { NodeAttrsDetails } from '../node_attrs_details';
interface Props {
phase: string;
setPhaseData: (dataKey: string, value: any) => void;
showNodeDetailsFlyout: (nodeAttrs: any) => void;
errors: any;
phaseData: any;
isShowingErrors: boolean;
@ -48,13 +48,16 @@ const learnMoreLink = (
export const NodeAllocation: React.FunctionComponent<Props> = ({
phase,
setPhaseData,
showNodeDetailsFlyout,
errors,
phaseData,
isShowingErrors,
}) => {
const { isLoading, data: nodes, error, sendRequest } = useLoadNodes();
const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState<string | null>(
null
);
if (isLoading) {
return (
<Fragment>
@ -162,7 +165,7 @@ export const NodeAllocation: React.FunctionComponent<Props> = ({
data-test-subj={`${phase}-viewNodeDetailsFlyoutButton`}
flush="left"
iconType="eye"
onClick={() => showNodeDetailsFlyout(phaseData[PHASE_NODE_ATTRS])}
onClick={() => setSelectedNodeAttrsForDetails(phaseData[PHASE_NODE_ATTRS])}
>
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.viewNodeDetailsButton"
@ -172,6 +175,13 @@ export const NodeAllocation: React.FunctionComponent<Props> = ({
) : null}
{learnMoreLink}
<EuiSpacer size="m" />
{selectedNodeAttrsForDetails ? (
<NodeAttrsDetails
selectedNodeAttrs={selectedNodeAttrsForDetails}
close={() => setSelectedNodeAttrsForDetails(null)}
/>
) : null}
</Fragment>
);
};

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { NodeAttrsDetails } from './node_attrs_details.container';
export { NodeAttrsDetails } from './node_attrs_details';

View file

@ -1,18 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { connect } from 'react-redux';
import { getNodeDetails } from '../../../../store/selectors';
import { fetchNodeDetails } from '../../../../store/actions';
import { NodeAttrsDetails as PresentationComponent } from './node_attrs_details';
export const NodeAttrsDetails = connect(
(state, ownProps) => ({
details: getNodeDetails(state, ownProps.selectedNodeAttrs),
}),
{ fetchNodeDetails }
)(PresentationComponent);

View file

@ -1,81 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlyoutBody,
EuiFlyout,
EuiTitle,
EuiInMemoryTable,
EuiSpacer,
EuiPortal,
} from '@elastic/eui';
export class NodeAttrsDetails extends PureComponent {
static propTypes = {
fetchNodeDetails: PropTypes.func.isRequired,
close: PropTypes.func.isRequired,
details: PropTypes.array,
selectedNodeAttrs: PropTypes.string.isRequired,
};
UNSAFE_componentWillMount() {
this.props.fetchNodeDetails(this.props.selectedNodeAttrs);
}
render() {
const { selectedNodeAttrs, details, close } = this.props;
return (
<EuiPortal>
<EuiFlyout ownFocus onClose={close}>
<EuiFlyoutBody>
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.indexLifecycleMgmt.nodeAttrDetails.title"
defaultMessage="Nodes that contain the attribute {selectedNodeAttrs}"
values={{ selectedNodeAttrs }}
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiInMemoryTable
items={details || []}
columns={[
{
field: 'nodeId',
name: i18n.translate('xpack.indexLifecycleMgmt.nodeAttrDetails.idField', {
defaultMessage: 'ID',
}),
},
{
field: 'stats.name',
name: i18n.translate('xpack.indexLifecycleMgmt.nodeAttrDetails.nameField', {
defaultMessage: 'Name',
}),
},
{
field: 'stats.host',
name: i18n.translate('xpack.indexLifecycleMgmt.nodeAttrDetails.hostField', {
defaultMessage: 'Host',
}),
},
]}
pagination={true}
sorting={true}
/>
</EuiFlyoutBody>
</EuiFlyout>
</EuiPortal>
);
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlyoutBody,
EuiFlyout,
EuiTitle,
EuiInMemoryTable,
EuiSpacer,
EuiPortal,
EuiLoadingContent,
EuiCallOut,
EuiButton,
} from '@elastic/eui';
import { useLoadNodeDetails } from '../../../../services/api';
interface Props {
close: () => void;
selectedNodeAttrs: string;
}
export const NodeAttrsDetails: React.FunctionComponent<Props> = ({ close, selectedNodeAttrs }) => {
const { data, isLoading, error, sendRequest } = useLoadNodeDetails(selectedNodeAttrs);
let content;
if (isLoading) {
content = <EuiLoadingContent lines={3} />;
} else if (error) {
const { statusCode, message } = error;
content = (
<EuiCallOut
title={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeDetailsLoadingFailedTitle"
defaultMessage="Unable to load node attribute details"
/>
}
color="danger"
>
<p>
{message} ({statusCode})
</p>
<EuiButton onClick={sendRequest} iconType="refresh" color="danger">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeDetailsReloadButton"
defaultMessage="Try again"
/>
</EuiButton>
</EuiCallOut>
);
} else {
content = (
<EuiInMemoryTable
items={data || []}
columns={[
{
field: 'nodeId',
name: i18n.translate('xpack.indexLifecycleMgmt.nodeAttrDetails.idField', {
defaultMessage: 'ID',
}),
},
{
field: 'stats.name',
name: i18n.translate('xpack.indexLifecycleMgmt.nodeAttrDetails.nameField', {
defaultMessage: 'Name',
}),
},
{
field: 'stats.host',
name: i18n.translate('xpack.indexLifecycleMgmt.nodeAttrDetails.hostField', {
defaultMessage: 'Host',
}),
},
]}
pagination={true}
sorting={true}
/>
);
}
return (
<EuiPortal>
<EuiFlyout ownFocus onClose={close}>
<EuiFlyoutBody>
<EuiTitle>
<h2>
<FormattedMessage
id="xpack.indexLifecycleMgmt.nodeAttrDetails.title"
defaultMessage="Nodes that contain the attribute {selectedNodeAttrs}"
values={{ selectedNodeAttrs }}
/>
</h2>
</EuiTitle>
<EuiSpacer size="s" />
{content}
</EuiFlyoutBody>
</EuiFlyout>
</EuiPortal>
);
};

View file

@ -38,7 +38,6 @@ import { MinAgeInput } from '../min_age_input';
export class WarmPhase extends PureComponent {
static propTypes = {
setPhaseData: PropTypes.func.isRequired,
showNodeDetailsFlyout: PropTypes.func.isRequired,
isShowingErrors: PropTypes.bool.isRequired,
errors: PropTypes.object.isRequired,
@ -47,7 +46,6 @@ export class WarmPhase extends PureComponent {
render() {
const {
setPhaseData,
showNodeDetailsFlyout,
phaseData,
errors,
isShowingErrors,
@ -152,7 +150,6 @@ export class WarmPhase extends PureComponent {
<NodeAllocation
phase={PHASE_WARM}
setPhaseData={setPhaseData}
showNodeDetailsFlyout={showNodeDetailsFlyout}
errors={errors}
phaseData={phaseData}
isShowingErrors={isShowingErrors}

View file

@ -38,7 +38,6 @@ import {
import { toasts } from '../../services/notification';
import { findFirstError } from '../../services/find_errors';
import { LearnMoreLink } from '../components';
import { NodeAttrsDetails } from './components/node_attrs_details';
import { PolicyJsonFlyout } from './components/policy_json_flyout';
import { ErrableFormRow } from './form_errors';
import { HotPhase } from './components/hot_phase';
@ -56,8 +55,6 @@ export class EditPolicy extends Component {
super(props);
this.state = {
isShowingErrors: false,
isShowingNodeDetailsFlyout: false,
selectedNodeAttrsForDetails: undefined,
isShowingPolicyJsonFlyout: false,
};
}
@ -124,10 +121,6 @@ export class EditPolicy extends Component {
}
};
showNodeDetailsFlyout = (selectedNodeAttrsForDetails) => {
this.setState({ isShowingNodeDetailsFlyout: true, selectedNodeAttrsForDetails });
};
togglePolicyJsonFlyout = () => {
this.setState(({ isShowingPolicyJsonFlyout }) => ({
isShowingPolicyJsonFlyout: !isShowingPolicyJsonFlyout,
@ -291,7 +284,6 @@ export class EditPolicy extends Component {
<WarmPhase
errors={errors[PHASE_WARM]}
showNodeDetailsFlyout={this.showNodeDetailsFlyout}
isShowingErrors={isShowingErrors && !!findFirstError(errors[PHASE_WARM], false)}
/>
@ -299,7 +291,6 @@ export class EditPolicy extends Component {
<ColdPhase
errors={errors[PHASE_COLD]}
showNodeDetailsFlyout={this.showNodeDetailsFlyout}
isShowingErrors={isShowingErrors && !!findFirstError(errors[PHASE_COLD], false)}
/>
@ -370,13 +361,6 @@ export class EditPolicy extends Component {
</EuiFlexItem>
</EuiFlexGroup>
{this.state.isShowingNodeDetailsFlyout ? (
<NodeAttrsDetails
selectedNodeAttrs={this.state.selectedNodeAttrsForDetails}
close={() => this.setState({ isShowingNodeDetailsFlyout: false })}
/>
) : null}
{this.state.isShowingPolicyJsonFlyout ? (
<PolicyJsonFlyout
policyName={selectedPolicyName || ''}

View file

@ -29,9 +29,12 @@ export const useLoadNodes = () => {
});
};
export async function loadNodeDetails(selectedNodeAttrs: string) {
return await sendGet(`nodes/${selectedNodeAttrs}/details`);
}
export const useLoadNodeDetails = (selectedNodeAttrs: string) => {
return useRequest({
path: `nodes/${selectedNodeAttrs}/details`,
method: 'get',
});
};
export async function loadIndexTemplates() {
return await sendGet(`templates`);

View file

@ -3,33 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { createAction } from 'redux-actions';
import { showApiError } from '../../services/api_errors';
import { loadNodeDetails } from '../../services/api';
import { SET_SELECTED_NODE_ATTRS } from '../../constants';
export const setSelectedNodeAttrs = createAction(SET_SELECTED_NODE_ATTRS);
export const setSelectedPrimaryShardCount = createAction('SET_SELECTED_PRIMARY_SHARED_COUNT');
export const setSelectedReplicaCount = createAction('SET_SELECTED_REPLICA_COUNT');
export const fetchedNodeDetails = createAction(
'FETCHED_NODE_DETAILS',
(selectedNodeAttrs, details) => ({
selectedNodeAttrs,
details,
})
);
export const fetchNodeDetails = (selectedNodeAttrs) => async (dispatch) => {
let details;
try {
details = await loadNodeDetails(selectedNodeAttrs);
} catch (err) {
const title = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.nodeDetailErrorMessage', {
defaultMessage: 'Error loading node attribute details',
});
showApiError(err, title);
return false;
}
dispatch(fetchedNodeDetails(selectedNodeAttrs, details));
};

View file

@ -5,12 +5,7 @@
*/
import { handleActions } from 'redux-actions';
import {
setSelectedNodeAttrs,
setSelectedPrimaryShardCount,
setSelectedReplicaCount,
fetchedNodeDetails,
} from '../actions/nodes';
import { setSelectedPrimaryShardCount, setSelectedReplicaCount } from '../actions';
const defaultState = {
isLoading: false,
@ -23,22 +18,6 @@ const defaultState = {
export const nodes = handleActions(
{
[fetchedNodeDetails](state, { payload }) {
const { selectedNodeAttrs, details } = payload;
return {
...state,
details: {
...state.details,
[selectedNodeAttrs]: details,
},
};
},
[setSelectedNodeAttrs](state, { payload: selectedNodeAttrs }) {
return {
...state,
selectedNodeAttrs,
};
},
[setSelectedPrimaryShardCount](state, { payload }) {
let selectedPrimaryShardCount = parseInt(payload);
if (isNaN(selectedPrimaryShardCount)) {

View file

@ -10,17 +10,3 @@ export const getSelectedPrimaryShardCount = (state) => state.nodes.selectedPrima
export const getSelectedReplicaCount = (state) =>
state.nodes.selectedReplicaCount !== undefined ? state.nodes.selectedReplicaCount : 1;
export const getSelectedNodeAttrs = (state) => state.nodes.selectedNodeAttrs;
export const getNodesFromSelectedNodeAttrs = (state) => {
const nodes = getNodes(state)[getSelectedNodeAttrs(state)];
if (nodes) {
return nodes.length;
}
return null;
};
export const getNodeDetails = (state, selectedNodeAttrs) => {
return state.nodes.details[selectedNodeAttrs];
};

View file

@ -8206,7 +8206,6 @@
"xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "シャードの割当をコントロールするノード属性を選択",
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "ノード属性なしではシャードの割り当てをコントロールできません。",
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml でノード属性が構成されていません",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailErrorMessage": "ノード属性の詳細の読み込み中にエラーが発生しました",
"xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字が必要です。",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "コールドフェーズのタイミング",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "コールドフェーズのタイミングの単位",

View file

@ -8208,7 +8208,6 @@
"xpack.indexLifecycleMgmt.editPolicy.nodeAllocationLabel": "选择节点属性来控制分片分配",
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingDescription": "没有节点属性,将无法控制分片分配。",
"xpack.indexLifecycleMgmt.editPolicy.nodeAttributesMissingLabel": "elasticsearch.yml 中未配置任何节点属性",
"xpack.indexLifecycleMgmt.editPolicy.nodeDetailErrorMessage": "加载节点属性详细信息时出错",
"xpack.indexLifecycleMgmt.editPolicy.numberRequiredError": "数字必填。",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeLabel": "冷阶段计时",
"xpack.indexLifecycleMgmt.editPolicy.phaseCold.minimumAgeUnitsAriaLabel": "冷阶段计时单位",