[ILM] Moved error and loading notices for data allocation (#85154)

* moved error and loading notices for data allocation field to below description

* removed test code

* expect controls to be showing, only render notice after network request has finished

* added loading spinner for field inputs

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2020-12-10 14:39:57 +01:00 committed by GitHub
parent cb29438b0d
commit f6c149f4f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 152 additions and 150 deletions

View file

@ -556,6 +556,7 @@ describe('edit policy', () => {
await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'warm');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(findTestSubject(rendered, 'warm-dataTierAllocationControls').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
expect(getNodeAttributeSelect(rendered, 'warm').exists()).toBeFalsy();
});
@ -684,6 +685,7 @@ describe('edit policy', () => {
await setPolicyName(rendered, 'mypolicy');
await activatePhase(rendered, 'cold');
expect(rendered.find('.euiLoadingSpinner').exists()).toBeTruthy();
expect(findTestSubject(rendered, 'cold-dataTierAllocationControls').exists()).toBeTruthy();
expect(rendered.find('.euiCallOut--warning').exists()).toBeFalsy();
expect(getNodeAttributeSelect(rendered, 'cold').exists()).toBeFalsy();
});

View file

@ -142,7 +142,7 @@ const getSelectOptions = (phase: PhaseWithAllocation, disableDataTierOption: boo
].filter(Boolean) as SelectOptions[];
export const DataTierAllocation: FunctionComponent<SharedProps> = (props) => {
const { phase, hasNodeAttributes, disableDataTierOption } = props;
const { phase, hasNodeAttributes, disableDataTierOption, isLoading } = props;
const dataTierAllocationTypePath = `_meta.${phase}.dataTierAllocationType`;
@ -170,6 +170,7 @@ export const DataTierAllocation: FunctionComponent<SharedProps> = (props) => {
<SuperSelectField
field={field}
euiFieldProps={{
isLoading,
hasDividers: true,
'data-test-subj': 'dataTierSelect',
options: getSelectOptions(phase, disableDataTierOption),

View file

@ -4,8 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { NodesDataProvider } from './node_data_provider';
export { NodeAllocation } from './node_allocation';
export { NodeAttrsDetails } from './node_attrs_details';
@ -17,3 +15,5 @@ export { DefaultAllocationNotice } from './default_allocation_notice';
export { NoNodeAttributesWarning } from './no_node_attributes_warning';
export { CloudDataTierCallout } from './cloud_data_tier_callout';
export { LoadingError } from './loading_error';

View file

@ -0,0 +1,47 @@
/*
* 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, { FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCallOut, EuiButton, EuiSpacer } from '@elastic/eui';
interface Props {
onResendRequest: () => void;
statusCode?: string | number;
message?: string;
}
export const LoadingError: FunctionComponent<Props> = ({
statusCode,
message,
onResendRequest,
}) => {
return (
<>
<EuiCallOut
title={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle"
defaultMessage="Unable to load node data"
/>
}
color="danger"
>
<p>
{message} ({statusCode})
</p>
<EuiButton onClick={onResendRequest} iconType="refresh" color="danger">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton"
defaultMessage="Try again"
/>
</EuiButton>
</EuiCallOut>
<EuiSpacer size="xl" />
</>
);
};

View file

@ -37,7 +37,7 @@ const i18nTexts = {
),
};
export const NodeAllocation: FunctionComponent<SharedProps> = ({ phase, nodes }) => {
export const NodeAllocation: FunctionComponent<SharedProps> = ({ phase, nodes, isLoading }) => {
const allocationNodeAttributePath = `_meta.${phase}.allocationNodeAttribute`;
const [formData] = useFormData({
@ -98,6 +98,7 @@ export const NodeAllocation: FunctionComponent<SharedProps> = ({ phase, nodes })
nodeOptions
),
hasNoInitialSelection: false,
isLoading,
},
}}
/>

View file

@ -1,70 +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 from 'react';
import { EuiButton, EuiCallOut, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ListNodesRouteResponse } from '../../../../../../../../../common/types';
import { useLoadNodes } from '../../../../../../../services/api';
interface Props {
children: (data: ListNodesRouteResponse) => JSX.Element;
}
export const NodesDataProvider = ({ children }: Props): JSX.Element => {
const { isLoading, data, error, resendRequest } = useLoadNodes();
if (isLoading) {
return (
<>
<EuiLoadingSpinner size="xl" />
<EuiSpacer size="m" />
</>
);
}
const renderError = () => {
if (error) {
const { statusCode, message } = error;
return (
<>
<EuiCallOut
style={{ maxWidth: 400 }}
title={
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAttributesLoadingFailedTitle"
defaultMessage="Unable to load node attributes"
/>
}
color="danger"
>
<p>
{message} ({statusCode})
</p>
<EuiButton onClick={resendRequest} iconType="refresh" color="danger">
<FormattedMessage
id="xpack.indexLifecycleMgmt.editPolicy.nodeAttributesReloadButton"
defaultMessage="Try again"
/>
</EuiButton>
</EuiCallOut>
<EuiSpacer size="xl" />
</>
);
}
return null;
};
return (
<>
{renderError()}
{/* `data` will always be defined because we use an initial value when loading */}
{children(data!)}
</>
);
};

View file

@ -19,4 +19,8 @@ export interface SharedProps {
* detected.
*/
disableDataTierOption: boolean;
/**
* A flag to indicate whether input fields should be showing a loading spinner
*/
isLoading: boolean;
}

View file

@ -7,8 +7,7 @@
import { get } from 'lodash';
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiDescribedFormGroup, EuiSpacer } from '@elastic/eui';
import { EuiDescribedFormGroup, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui';
import { useKibana, useFormData } from '../../../../../../../shared_imports';
@ -18,14 +17,16 @@ import { getAvailableNodeRoleForPhase } from '../../../../../../lib/data_tiers';
import { isNodeRoleFirstPreference } from '../../../../../../lib';
import { useLoadNodes } from '../../../../../../services/api';
import { DataTierAllocationType } from '../../../../types';
import {
DataTierAllocation,
DefaultAllocationNotice,
NoNodeAttributesWarning,
NodesDataProvider,
CloudDataTierCallout,
LoadingError,
} from './components';
import './_data_tier_allocation.scss';
@ -53,85 +54,101 @@ export const DataTierAllocationField: FunctionComponent<Props> = ({ phase, descr
const [formData] = useFormData({ watch: dataTierAllocationTypePath });
const allocationType: DataTierAllocationType = get(formData, dataTierAllocationTypePath);
return (
<NodesDataProvider>
{({ nodesByRoles, nodesByAttributes, isUsingDeprecatedDataRoleConfig }) => {
const hasDataNodeRoles = Object.keys(nodesByRoles).some((nodeRole) =>
// match any of the "data_" roles, including data_content.
nodeRole.trim().startsWith('data_')
);
const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length);
const isCloudEnabled = cloud?.isCloudEnabled ?? false;
const { data, resendRequest, error, isLoading } = useLoadNodes();
const renderNotice = () => {
switch (allocationType) {
case 'node_roles':
if (isCloudEnabled && phase === 'cold') {
const isUsingNodeRolesAllocation =
!isUsingDeprecatedDataRoleConfig && hasDataNodeRoles;
const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length;
const { nodesByRoles, nodesByAttributes, isUsingDeprecatedDataRoleConfig } = data!;
if (isUsingNodeRolesAllocation && hasNoNodesWithNodeRole) {
// Tell cloud users they can deploy nodes on cloud.
return (
<>
<EuiSpacer size="s" />
<CloudDataTierCallout />
</>
);
}
}
const hasDataNodeRoles = Object.keys(nodesByRoles).some((nodeRole) =>
// match any of the "data_" roles, including data_content.
nodeRole.trim().startsWith('data_')
);
const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length);
const isCloudEnabled = cloud?.isCloudEnabled ?? false;
const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles);
if (
allocationNodeRole === 'none' ||
!isNodeRoleFirstPreference(phase, allocationNodeRole)
) {
return (
<>
<EuiSpacer size="s" />
<DefaultAllocationNotice phase={phase} targetNodeRole={allocationNodeRole} />
</>
);
}
break;
case 'node_attrs':
if (!hasNodeAttrs) {
return (
<>
<EuiSpacer size="s" />
<NoNodeAttributesWarning phase={phase} />
</>
);
}
break;
default:
return null;
const renderNotice = () => {
switch (allocationType) {
case 'node_roles':
if (isCloudEnabled && phase === 'cold') {
const isUsingNodeRolesAllocation = !isUsingDeprecatedDataRoleConfig && hasDataNodeRoles;
const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length;
if (isUsingNodeRolesAllocation && hasNoNodesWithNodeRole) {
// Tell cloud users they can deploy nodes on cloud.
return (
<>
<EuiSpacer size="s" />
<CloudDataTierCallout />
</>
);
}
};
}
return (
<EuiDescribedFormGroup
title={<h3>{i18nTexts.title}</h3>}
description={description}
fullWidth
>
<div className="ilmDataTierAllocationField">
<DataTierAllocation
hasNodeAttributes={hasNodeAttrs}
phase={phase}
nodes={nodesByAttributes}
disableDataTierOption={Boolean(
isCloudEnabled && !hasDataNodeRoles && isUsingDeprecatedDataRoleConfig
)}
const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles);
if (
allocationNodeRole === 'none' ||
!isNodeRoleFirstPreference(phase, allocationNodeRole)
) {
return (
<>
<EuiSpacer size="s" />
<DefaultAllocationNotice phase={phase} targetNodeRole={allocationNodeRole} />
</>
);
}
break;
case 'node_attrs':
if (!hasNodeAttrs) {
return (
<>
<EuiSpacer size="s" />
<NoNodeAttributesWarning phase={phase} />
</>
);
}
break;
default:
return null;
}
};
return (
<EuiDescribedFormGroup
title={<h3>{i18nTexts.title}</h3>}
description={
<>
{description}
{isLoading ? (
<>
<EuiSpacer size="m" />
<EuiLoadingSpinner size="xl" />
</>
) : (
error && (
<LoadingError
onResendRequest={resendRequest}
message={error.message}
statusCode={error.statusCode}
/>
)
)}
</>
}
fullWidth
>
<div className="ilmDataTierAllocationField">
<DataTierAllocation
hasNodeAttributes={hasNodeAttrs}
phase={phase}
nodes={nodesByAttributes}
disableDataTierOption={Boolean(
isCloudEnabled && !hasDataNodeRoles && isUsingDeprecatedDataRoleConfig
)}
isLoading={isLoading}
/>
{/* Data tier related warnings and call-to-action notices */}
{renderNotice()}
</div>
</EuiDescribedFormGroup>
);
}}
</NodesDataProvider>
{/* Data tier related warnings and call-to-action notices */}
{!isLoading && renderNotice()}
</div>
</EuiDescribedFormGroup>
);
};