Fix bugs with Create Index Pattern wizard loading state and createReasonableWait UX helper (#16895)

* Fix bugs with Create Index Pattern wizard loading state and createReasonableWait UX helper.
* Fix snapshots and refactor tests for clarity.
* Rename createReasonableWait to ensureMinimumTime, refactor for clarity, and support a time argument.
* Update form error copy.
This commit is contained in:
CJ Cenizal 2018-03-09 14:20:38 -08:00 committed by GitHub
parent 0caa5a8acf
commit 8cef9135eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 397 additions and 747 deletions

View file

@ -1,6 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CreateIndexPatternWizard should render step 1 1`] = `
exports[`CreateIndexPatternWizard defaults to the loading state 1`] = `
<div>
<Header
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
/>
<LoadingState />
</div>
`;
exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = `
<div>
<Header
isIncludingSystemIndices={false}
@ -9,9 +19,7 @@ exports[`CreateIndexPatternWizard should render step 1 1`] = `
<StepIndexPattern
allIndices={
Array [
Object {
"name": "kibana",
},
Object {},
]
}
esService={Object {}}
@ -23,7 +31,20 @@ exports[`CreateIndexPatternWizard should render step 1 1`] = `
</div>
`;
exports[`CreateIndexPatternWizard should render step 2 1`] = `
exports[`CreateIndexPatternWizard renders the empty state when there are no indices 1`] = `
<div>
<Header
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
/>
<EmptyState
loadingDataDocUrl=""
onRefresh={[Function]}
/>
</div>
`;
exports[`CreateIndexPatternWizard renders time field step when step is set to 2 1`] = `
<div>
<Header
isIncludingSystemIndices={false}
@ -37,33 +58,3 @@ exports[`CreateIndexPatternWizard should render step 2 1`] = `
/>
</div>
`;
exports[`CreateIndexPatternWizard should render the empty state 1`] = `
<div>
<Header
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
/>
<EmptyState
loadingDataDocUrl=""
/>
<StepIndexPattern
allIndices={Array []}
esService={Object {}}
goToNextStep={[Function]}
initialQuery=""
isIncludingSystemIndices={false}
savedObjectsClient={Object {}}
/>
</div>
`;
exports[`CreateIndexPatternWizard should render the loading state 1`] = `
<div>
<Header
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
/>
<LoadingState />
</div>
`;

View file

@ -28,43 +28,7 @@ const services = {
};
describe('CreateIndexPatternWizard', () => {
it('should render step 1', async () => {
const component = shallow(
<CreateIndexPatternWizard
loadingDataDocUrl={loadingDataDocUrl}
initialQuery={initialQuery}
services={services}
/>
);
// Allow the componentWillMount code to execute
// https://github.com/airbnb/enzyme/issues/450
await component.update(); // Fire `componentWillMount()`
await component.update(); // Force update the component post async actions
expect(component).toMatchSnapshot();
});
it('should render step 2', async () => {
const component = shallow(
<CreateIndexPatternWizard
loadingDataDocUrl={loadingDataDocUrl}
initialQuery={initialQuery}
services={services}
/>
);
// Allow the componentWillMount code to execute
// https://github.com/airbnb/enzyme/issues/450
await component.update(); // Fire `componentWillMount()`
await component.update(); // Force update the component post async actions
component.setState({ step: 2 });
expect(component).toMatchSnapshot();
});
it('should render the loading state', async () => {
it(`defaults to the loading state`, async () => {
const component = shallow(
<CreateIndexPatternWizard
loadingDataDocUrl={loadingDataDocUrl}
@ -76,7 +40,7 @@ describe('CreateIndexPatternWizard', () => {
expect(component).toMatchSnapshot();
});
it('should render the empty state', async () => {
it('renders the empty state when there are no indices', async () => {
const component = shallow(
<CreateIndexPatternWizard
loadingDataDocUrl={loadingDataDocUrl}
@ -85,18 +49,53 @@ describe('CreateIndexPatternWizard', () => {
/>
);
// Allow the componentWillMount code to execute
// https://github.com/airbnb/enzyme/issues/450
await component.update(); // Fire `componentWillMount()`
await component.update(); // Force update the component post async actions
// Remove all indices
component.setState({ allIndices: [] });
component.setState({
isInitiallyLoadingIndices: false,
allIndices: [],
});
await component.update();
expect(component).toMatchSnapshot();
});
it('should create an index pattern', async () => {
it('renders index pattern step when there are indices', async () => {
const component = shallow(
<CreateIndexPatternWizard
loadingDataDocUrl={loadingDataDocUrl}
initialQuery={initialQuery}
services={services}
/>
);
component.setState({
isInitiallyLoadingIndices: false,
allIndices: [{}],
});
await component.update();
expect(component).toMatchSnapshot();
});
it('renders time field step when step is set to 2', async () => {
const component = shallow(
<CreateIndexPatternWizard
loadingDataDocUrl={loadingDataDocUrl}
initialQuery={initialQuery}
services={services}
/>
);
component.setState({
isInitiallyLoadingIndices: false,
allIndices: [{}],
step: 2,
});
await component.update();
expect(component).toMatchSnapshot();
});
it('invokes the provided services when creating an index pattern', async () => {
const get = jest.fn();
const set = jest.fn();
const create = jest.fn().mockImplementation(() => 'id');

View file

@ -55,7 +55,7 @@ exports[`EmptyState should render normally 1`] = `
</p>
</EuiText>
<EuiSpacer
size="xs"
size="m"
/>
<EuiFlexGroup
alignItems="center"
@ -71,9 +71,11 @@ exports[`EmptyState should render normally 1`] = `
>
<EuiButton
color="primary"
data-test-subj="refreshIndicesButton"
fill={false}
iconSide="left"
iconType="faceHappy"
iconType="refresh"
onClick={[Function]}
type="button"
>
Check for new data

View file

@ -1,15 +1,36 @@
import React from 'react';
import { EmptyState } from '../empty_state';
import { shallow } from 'enzyme';
import sinon from 'sinon';
describe('EmptyState', () => {
it('should render normally', () => {
const component = shallow(
<EmptyState
loadingDataDocUrl="http://www.elastic.co"
onRefresh={() => {}}
/>
);
expect(component).toMatchSnapshot();
});
describe('props', () => {
describe('onRefresh', () => {
it('is called when refresh button is clicked', () => {
const onRefreshHandler = sinon.stub();
const component = shallow(
<EmptyState
loadingDataDocUrl="http://www.elastic.co"
onRefresh={onRefreshHandler}
/>
);
component.find('[data-test-subj="refreshIndicesButton"]').simulate('click');
sinon.assert.calledOnce(onRefreshHandler);
});
});
});
});

View file

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
EuiPanel,
@ -14,6 +15,7 @@ import {
export const EmptyState = ({
loadingDataDocUrl,
onRefresh,
}) => (
<EuiPanel paddingSize="l">
<EuiFlexGroup justifyContent="center" alignItems="center">
@ -38,10 +40,16 @@ export const EmptyState = ({
</EuiLink>
</p>
</EuiText>
<EuiSpacer size="xs"/>
<EuiSpacer size="m"/>
<EuiFlexGroup justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton iconType="faceHappy">
<EuiButton
iconType="refresh"
onClick={onRefresh}
data-test-subj="refreshIndicesButton"
>
Check for new data
</EuiButton>
</EuiFlexItem>
@ -50,3 +58,8 @@ export const EmptyState = ({
</EuiFlexGroup>
</EuiPanel>
);
EmptyState.propTypes = {
loadingDataDocUrl: PropTypes.string.isRequired,
onRefresh: PropTypes.func.isRequired,
};

View file

@ -36,27 +36,34 @@ exports[`LoadingState should render normally 1`] = `
<EuiSpacer
size="s"
/>
<EuiText
size="s"
<EuiFlexGroup
alignItems="center"
component="div"
gutterSize="s"
justifyContent="center"
responsive={true}
wrap={false}
>
<p
style={
Object {
"textAlign": "center",
}
}
<EuiFlexItem
component="div"
grow={false}
>
<EuiIcon
size="m"
type="faceSad"
<EuiLoadingSpinner
size="l"
/>
<EuiTextColor
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiText
color="subdued"
size="s"
>
Reticulating splines...
</EuiTextColor>
</p>
</EuiText>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>

View file

@ -1,19 +1,17 @@
import React from 'react';
import {
EuiPanel,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiTitle,
EuiTextColor,
EuiLoadingSpinner,
EuiPanel,
EuiSpacer,
EuiIcon,
EuiText,
EuiTextColor,
EuiTitle,
} from '@elastic/eui';
export const LoadingState = ({
}) => (
export const LoadingState = () => (
<EuiPanel paddingSize="l">
<EuiFlexGroup justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
@ -22,15 +20,20 @@ export const LoadingState = ({
<h2 style={{ textAlign: 'center' }}>Checking for Elasticsearch data</h2>
</EuiTextColor>
</EuiTitle>
<EuiSpacer size="s"/>
<EuiText size="s">
<p style={{ textAlign: 'center' }}>
<EuiIcon type="faceSad"/>
<EuiTextColor color="subdued">
<EuiFlexGroup justifyContent="center" alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="l"/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" color="subdued">
Reticulating splines...
</EuiTextColor>
</p>
</EuiText>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>

View file

@ -1,386 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StepIndexPattern should disable the next step if the index pattern exists 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<Header
characterList="\\\\, /, ?, \\", <, >, |"
errors={Array []}
goToNextStep={[Function]}
isInputInvalid={false}
isNextStepDisabled={false}
onQueryChanged={[Function]}
query="k*"
/>
<EuiSpacer
size="s"
/>
<StatusMessage
matchedIndices={
exports[`StepIndexPattern renders errors when input is invalid 1`] = `
<Header
characterList="\\\\, /, ?, \\", <, >, |"
data-test-subj="createIndexPatternStep1Header"
errors={
Array [
"An index pattern cannot contain spaces or the characters: \\\\, /, ?, \\", <, >, |",
]
}
goToNextStep={[Function]}
isInputInvalid={true}
isNextStepDisabled={true}
onQueryChanged={[Function]}
query="?"
/>
`;
exports[`StepIndexPattern renders indices which match the initial query 1`] = `
<IndicesList
data-test-subj="createIndexPatternStep1IndicesList"
indices={
Array [
Object {
"allIndices": Array [
Object {
"name": "kibana",
},
Object {
"name": "es",
},
],
"exactMatchedIndices": Array [
Object {
"name": "kibana",
},
],
"partialMatchedIndices": Array [],
"visibleIndices": Array [
Object {
"name": "kibana",
},
],
}
}
query="k*"
/>
<EuiSpacer
size="s"
/>
<IndicesList
indices={
Array [
Object {
"name": "kibana",
},
]
}
query="k*"
/>
</EuiPanel>
"name": "kibana",
},
]
}
query="kibana"
/>
`;
exports[`StepIndexPattern should ensure we properly append a wildcard 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<Header
characterList="\\\\, /, ?, \\", <, >, |"
errors={Array []}
goToNextStep={[Function]}
isInputInvalid={false}
isNextStepDisabled={true}
onQueryChanged={[Function]}
query="k*"
/>
<EuiSpacer
size="s"
/>
<LoadingIndices />
<EuiSpacer
size="s"
/>
</EuiPanel>
`;
exports[`StepIndexPattern should properly fetch indices for the initial query 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<Header
characterList="\\\\, /, ?, \\", <, >, |"
errors={Array []}
goToNextStep={[Function]}
isInputInvalid={false}
isNextStepDisabled={false}
onQueryChanged={[Function]}
query="k*"
/>
<EuiSpacer
size="s"
/>
<StatusMessage
matchedIndices={
exports[`StepIndexPattern renders matching indices when input is valid 1`] = `
<IndicesList
data-test-subj="createIndexPatternStep1IndicesList"
indices={
Array [
Object {
"allIndices": Array [
Object {
"name": "kibana",
},
Object {
"name": "es",
},
],
"exactMatchedIndices": Array [
Object {
"name": "kibana",
},
],
"partialMatchedIndices": Array [],
"visibleIndices": Array [
Object {
"name": "kibana",
},
],
}
}
query="k*"
/>
<EuiSpacer
size="s"
/>
<IndicesList
indices={
Array [
Object {
"name": "kibana",
},
]
}
query="k*"
/>
</EuiPanel>
"name": "kibana",
},
]
}
query="k*"
/>
`;
exports[`StepIndexPattern should render normally 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<Header
characterList="\\\\, /, ?, \\", <, >, |"
errors={Array []}
goToNextStep={[Function]}
isInputInvalid={false}
isNextStepDisabled={true}
onQueryChanged={[Function]}
query=""
/>
<EuiSpacer
size="s"
/>
<StatusMessage
matchedIndices={
Object {
"allIndices": Array [
Object {
"name": "kibana",
},
Object {
"name": "es",
},
],
"exactMatchedIndices": Array [],
"partialMatchedIndices": Array [],
"visibleIndices": Array [
Object {
"name": "kibana",
},
Object {
"name": "es",
},
],
}
}
query=""
/>
<EuiSpacer
size="s"
/>
<IndicesList
indices={
Array [
Object {
"name": "kibana",
},
Object {
"name": "es",
},
]
}
query=""
/>
</EuiPanel>
`;
exports[`StepIndexPattern should render some indices 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<Header
characterList="\\\\, /, ?, \\", <, >, |"
errors={Array []}
goToNextStep={[Function]}
isInputInvalid={false}
isNextStepDisabled={false}
onQueryChanged={[Function]}
query="k*"
/>
<EuiSpacer
size="s"
/>
<StatusMessage
matchedIndices={
Object {
"allIndices": Array [
Object {
"name": "kibana",
},
Object {
"name": "es",
},
],
"exactMatchedIndices": Array [
Object {
"name": "kibana",
},
],
"partialMatchedIndices": Array [],
"visibleIndices": Array [
Object {
"name": "kibana",
},
],
}
}
query="k*"
/>
<EuiSpacer
size="s"
/>
<IndicesList
indices={
Array [
Object {
"name": "kibana",
},
]
}
query="k*"
/>
</EuiPanel>
`;
exports[`StepIndexPattern should render the loading state 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<Header
characterList="\\\\, /, ?, \\", <, >, |"
errors={Array []}
goToNextStep={[Function]}
isInputInvalid={false}
isNextStepDisabled={true}
onQueryChanged={[Function]}
query="k"
/>
<EuiSpacer
size="s"
/>
<LoadingIndices />
<EuiSpacer
size="s"
/>
</EuiPanel>
`;
exports[`StepIndexPattern should search for partial indices for queries not ending in a wildcard 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<Header
characterList="\\\\, /, ?, \\", <, >, |"
errors={Array []}
goToNextStep={[Function]}
isInputInvalid={false}
isNextStepDisabled={false}
onQueryChanged={[Function]}
query="k"
/>
<EuiSpacer
size="s"
/>
<StatusMessage
matchedIndices={
Object {
"allIndices": Array [
Object {
"name": "kibana",
},
Object {
"name": "es",
},
],
"exactMatchedIndices": Array [
Object {
"name": "kibana",
},
],
"partialMatchedIndices": Array [
Object {
"name": "kibana",
},
],
"visibleIndices": Array [
Object {
"name": "kibana",
},
],
}
}
query="k"
/>
<EuiSpacer
size="s"
/>
<IndicesList
indices={
Array [
Object {
"name": "kibana",
},
]
}
query="k"
/>
</EuiPanel>
`;
exports[`StepIndexPattern should show errors 1`] = `
<EuiPanel
grow={true}
hasShadow={false}
paddingSize="l"
>
<Header
characterList="\\\\, /, ?, \\", <, >, |"
errors={
Array [
"Your input contains invalid characters or spaces. Please omit: \\\\, /, ?, \\", <, >, |",
]
}
goToNextStep={[Function]}
isInputInvalid={true}
isNextStepDisabled={true}
onQueryChanged={[Function]}
query="?"
/>
<EuiSpacer
size="s"
/>
<LoadingIndices />
<EuiSpacer
size="s"
/>
</EuiPanel>
exports[`StepIndexPattern renders the loading state 1`] = `
<LoadingIndices
data-test-subj="createIndexPatternStep1Loading"
/>
`;

View file

@ -1,13 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import { StepIndexPattern } from '../step_index_pattern';
jest.mock('../components/indices_list', () => ({ IndicesList: 'IndicesList' }));
jest.mock('../components/loading_indices', () => ({ LoadingIndices: 'LoadingIndices' }));
jest.mock('../components/status_message', () => ({ StatusMessage: 'StatusMessage' }));
jest.mock('../components/header', () => ({ Header: 'Header' }));
jest.mock('../../../lib/create_reasonable_wait', () => ({ createReasonableWait: fn => fn() }));
jest.mock('../../../lib/ensure_minimum_time', () => ({
ensureMinimumTime: async (promises) => Array.isArray(promises) ? await Promise.all(promises) : await promises
}));
jest.mock('../../../lib/get_indices', () => ({
getIndices: () => {
return [
@ -23,166 +20,73 @@ const savedObjectsClient = {
};
const goToNextStep = () => {};
const createComponent = props => {
return shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
savedObjectsClient={savedObjectsClient}
goToNextStep={goToNextStep}
{...props}
/>
);
};
describe('StepIndexPattern', () => {
it('should render normally', () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
savedObjectsClient={savedObjectsClient}
goToNextStep={goToNextStep}
/>
);
expect(component).toMatchSnapshot();
it('renders the loading state', () => {
const component = createComponent();
component.setState({ isLoadingIndices: true });
expect(component.find('[data-test-subj="createIndexPatternStep1Loading"]')).toMatchSnapshot();
});
it('should render the loading state', () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
savedObjectsClient={savedObjectsClient}
goToNextStep={goToNextStep}
/>
);
component.setState({ query: 'k', isLoadingIndices: true });
expect(component).toMatchSnapshot();
});
it('should render some indices', async () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
savedObjectsClient={savedObjectsClient}
goToNextStep={goToNextStep}
/>
);
const instance = component.instance();
await instance.onQueryChanged({
target: { value: 'k' }
});
it('renders indices which match the initial query', async () => {
const component = createComponent({ initialQuery: 'kibana' });
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
await component.update();
expect(component).toMatchSnapshot();
expect(component.find('[data-test-subj="createIndexPatternStep1IndicesList"]')).toMatchSnapshot();
});
it('should show errors', async () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
savedObjectsClient={savedObjectsClient}
goToNextStep={goToNextStep}
/>
);
it('renders errors when input is invalid', async () => {
const component = createComponent();
const instance = component.instance();
instance.onQueryChanged({ target: { value: '?' } });
await instance.onQueryChanged({
target: { value: '?' }
});
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component).toMatchSnapshot();
expect(component.find('[data-test-subj="createIndexPatternStep1Header"]')).toMatchSnapshot();
});
it('should properly fetch indices for the initial query', async () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
savedObjectsClient={savedObjectsClient}
goToNextStep={goToNextStep}
initialQuery="k*"
/>
);
// Allow the componentWillMount code to execute
// https://github.com/airbnb/enzyme/issues/450
await component.update(); // Fire `componentWillMount()`
await component.update(); // Force update the component post async actions
expect(component).toMatchSnapshot();
});
it('should disable the next step if the index pattern exists', async () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
savedObjectsClient={{
find: () => ({ savedObjects: [
{ attributes: { title: 'k*' } }
] })
}}
goToNextStep={goToNextStep}
initialQuery="k*"
/>
);
// Allow the componentWillMount code to execute
// https://github.com/airbnb/enzyme/issues/450
await component.update(); // Fire `componentWillMount()`
await component.update(); // Force update the component post async actions
expect(component).toMatchSnapshot();
});
it('should ensure we properly append a wildcard', async () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
savedObjectsClient={{
find: () => ({ savedObjects: [
{ attributes: { title: 'k*' } }
] })
}}
goToNextStep={goToNextStep}
/>
);
it('renders matching indices when input is valid', async () => {
const component = createComponent();
const instance = component.instance();
instance.onQueryChanged({ target: { value: 'k' } });
await component.update();
expect(component).toMatchSnapshot();
// Ensure all promises resolve
await new Promise(resolve => process.nextTick(resolve));
// Ensure the state changes are reflected
component.update();
expect(component.find('[data-test-subj="createIndexPatternStep1IndicesList"]')).toMatchSnapshot();
});
it('should search for partial indices for queries not ending in a wildcard', async () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
savedObjectsClient={savedObjectsClient}
goToNextStep={goToNextStep}
initialQuery="k"
/>
);
it('appends a wildcard automatically to queries', async () => {
const component = createComponent();
const instance = component.instance();
instance.onQueryChanged({ target: { value: 'k' } });
expect(component.state('query')).toBe('k*');
});
// Allow the componentWillMount code to execute
// https://github.com/airbnb/enzyme/issues/450
await component.update(); // Fire `componentWillMount()`
await component.update(); // Force update the component post async actions
await component.update(); // There are two actions so we apparently need to call this again
expect(component).toMatchSnapshot();
it('disables the next step if the index pattern exists', async () => {
const component = createComponent();
component.setState({ indexPatternExists: true });
expect(component.find('Header').prop('isNextStepDisabled')).toBe(true);
});
});

View file

@ -45,7 +45,7 @@ exports[`Header should mark the input as invalid 1`] = `
as a wildcard in your index pattern.
</p>
<p>
You can't use empty spaces or the characters
You can't use spaces or the characters
<strong>
%
</strong>
@ -131,7 +131,7 @@ exports[`Header should render normally 1`] = `
as a wildcard in your index pattern.
</p>
<p>
You can't use empty spaces or the characters
You can't use spaces or the characters
<strong>
%
</strong>

View file

@ -19,8 +19,9 @@ export const Header = ({
onQueryChanged,
goToNextStep,
isNextStepDisabled,
...rest
}) => (
<div>
<div {...rest}>
<EuiTitle size="s">
<h2>
Step 1 of 2: Define index pattern
@ -39,7 +40,7 @@ export const Header = ({
helpText={
<div>
<p>You can use a <strong>*</strong> as a wildcard in your index pattern.</p>
<p>You can&apos;t use empty spaces or the characters <strong>{characterList}</strong>.</p>
<p>You can&apos;t use spaces or the characters <strong>{characterList}</strong>.</p>
</div>
}
>

View file

@ -149,7 +149,7 @@ export class IndicesList extends Component {
}
render() {
const { indices, query } = this.props;
const { indices, query, ...rest } = this.props;
const queryWithoutWildcard = query.endsWith('*') ? query.substr(0, query.length - 1) : query;
@ -165,7 +165,7 @@ export class IndicesList extends Component {
});
return (
<div>
<div {...rest}>
<EuiTable>
<EuiTableBody>
{rows}

View file

@ -8,17 +8,23 @@ import {
EuiLoadingSpinner,
} from '@elastic/eui';
export const LoadingIndices = () => (
<EuiFlexGroup justifyContent="center" alignItems="center">
export const LoadingIndices = ({ ...rest }) => (
<EuiFlexGroup
justifyContent="center"
alignItems="center"
{...rest}
>
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<EuiTextColor color="subdued">
Looking for matching indices...
</EuiTextColor>
</EuiText>
<EuiText size="s" style={{ textAlign: 'center' }}>
<EuiTextColor color="subdued">
Just a sec...

View file

@ -6,7 +6,7 @@ import {
containsInvalidCharacters,
getMatchedIndices,
canAppendWildcard,
createReasonableWait
ensureMinimumTime
} from '../../lib';
import { LoadingIndices } from './components/loading_indices';
import { StatusMessage } from './components/status_message';
@ -74,19 +74,26 @@ export class StepIndexPattern extends Component {
}
this.setState({ isLoadingIndices: true, indexPatternExists: false });
if (query.endsWith('*')) {
const exactMatchedIndices = await getIndices(esService, query, MAX_SEARCH_SIZE);
createReasonableWait(() => this.setState({ exactMatchedIndices, isLoadingIndices: false }));
}
else {
const partialMatchedIndices = await getIndices(esService, `${query}*`, MAX_SEARCH_SIZE);
const exactMatchedIndices = await getIndices(esService, query, MAX_SEARCH_SIZE);
createReasonableWait(() => this.setState({
partialMatchedIndices,
exactMatchedIndices,
isLoadingIndices: false
}));
const exactMatchedIndices = await ensureMinimumTime(getIndices(esService, query, MAX_SEARCH_SIZE));
this.setState({ exactMatchedIndices, isLoadingIndices: false });
return;
}
const [
partialMatchedIndices,
exactMatchedIndices,
] = await ensureMinimumTime([
getIndices(esService, `${query}*`, MAX_SEARCH_SIZE),
getIndices(esService, query, MAX_SEARCH_SIZE),
]);
this.setState({
partialMatchedIndices,
exactMatchedIndices,
isLoadingIndices: false
});
}
onQueryChanged = e => {
@ -98,8 +105,7 @@ export class StepIndexPattern extends Component {
query += '*';
this.setState({ appendedWildcard: true });
setTimeout(() => target.setSelectionRange(1, 1));
}
else {
} else {
if (query === '*' && appendedWildcard) {
query = '';
this.setState({ appendedWildcard: false });
@ -118,7 +124,7 @@ export class StepIndexPattern extends Component {
}
return (
<LoadingIndices/>
<LoadingIndices data-test-subj="createIndexPatternStep1Loading" />
);
}
@ -151,6 +157,7 @@ export class StepIndexPattern extends Component {
return (
<IndicesList
data-test-subj="createIndexPatternStep1IndicesList"
query={query}
indices={indicesToList}
/>
@ -188,7 +195,7 @@ export class StepIndexPattern extends Component {
containsErrors = true;
}
else if (!containsInvalidCharacters(query, ILLEGAL_CHARACTERS)) {
errors.push(`Your input contains invalid characters or spaces. Please omit: ${characterList}`);
errors.push(`An index pattern cannot contain spaces or the characters: ${characterList}`);
containsErrors = true;
}
@ -197,6 +204,7 @@ export class StepIndexPattern extends Component {
return (
<Header
data-test-subj="createIndexPatternStep1Header"
isInputInvalid={isInputInvalid}
errors={errors}
characterList={characterList}

View file

@ -57,12 +57,12 @@ exports[`StepTimeField should render a selected timeField 1`] = `
"value": "",
},
Object {
"isDisabled": undefined,
"disabled": undefined,
"text": "@timestamp",
"value": "@timestamp",
},
Object {
"isDisabled": undefined,
"disabled": undefined,
"text": "name",
"value": "name",
},
@ -251,12 +251,12 @@ exports[`StepTimeField should render timeFields 1`] = `
"value": "",
},
Object {
"isDisabled": undefined,
"disabled": undefined,
"text": "@timestamp",
"value": "@timestamp",
},
Object {
"isDisabled": undefined,
"disabled": undefined,
"text": "name",
"value": "name",
},

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { extractTimeFields } from '../../lib/extract_time_fields';
import { ensureMinimumTime, extractTimeFields } from '../../lib';
import { Header } from './components/header';
import { TimeField } from './components/time_field';
@ -16,7 +16,6 @@ import {
EuiLoadingSpinner,
} from '@elastic/eui';
export class StepTimeField extends Component {
static propTypes = {
indexPattern: PropTypes.string.isRequired,
@ -46,7 +45,7 @@ export class StepTimeField extends Component {
const { indexPatternsService, indexPattern } = this.props;
this.setState({ isFetchingTimeFields: true });
const fields = await indexPatternsService.fieldsFetcher.fetchForWildcard(indexPattern);
const fields = await ensureMinimumTime(indexPatternsService.fieldsFetcher.fetchForWildcard(indexPattern));
const timeFields = extractTimeFields(fields);
this.setState({ timeFields, isFetchingTimeFields: false });
@ -108,7 +107,7 @@ export class StepTimeField extends Component {
...timeFields.map(timeField => ({
text: timeField.display,
value: timeField.fieldName || '',
isDisabled: timeFields.isDisabled,
disabled: timeFields.isDisabled,
}))
]
: [];

View file

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ensureMinimumTime } from './lib';
import { StepIndexPattern } from './components/step_index_pattern';
import { StepTimeField } from './components/step_time_field';
import { Header } from './components/header';
@ -35,8 +36,13 @@ export class CreateIndexPatternWizard extends Component {
}
async componentWillMount() {
this.fetchIndices();
}
fetchIndices = async () => {
this.setState({ allIndices: [], isInitiallyLoadingIndices: true });
const { services } = this.props;
const allIndices = await getIndices(services.es, `*`, MAX_SEARCH_SIZE);
const allIndices = await ensureMinimumTime(getIndices(services.es, `*`, MAX_SEARCH_SIZE));
this.setState({ allIndices, isInitiallyLoadingIndices: false });
}
@ -87,32 +93,7 @@ export class CreateIndexPatternWizard extends Component {
);
}
renderInitialLoadingState() {
const { isInitiallyLoadingIndices } = this.state;
if (!isInitiallyLoadingIndices) {
return null;
}
return (
<LoadingState/>
);
}
renderInitialEmptyState() {
const { allIndices, isInitiallyLoadingIndices } = this.state;
const { loadingDataDocUrl } = this.props;
if (allIndices.length > 0 || isInitiallyLoadingIndices) {
return null;
}
return (
<EmptyState loadingDataDocUrl={loadingDataDocUrl}/>
);
}
renderStepOne() {
renderContent() {
const {
allIndices,
isInitiallyLoadingIndices,
@ -121,50 +102,52 @@ export class CreateIndexPatternWizard extends Component {
indexPattern,
} = this.state;
if (isInitiallyLoadingIndices || step !== 1) {
return null;
if (isInitiallyLoadingIndices) {
return <LoadingState />;
}
const { services, initialQuery } = this.props;
return (
<StepIndexPattern
allIndices={allIndices}
initialQuery={indexPattern || initialQuery}
isIncludingSystemIndices={isIncludingSystemIndices}
esService={services.es}
savedObjectsClient={services.savedObjectsClient}
goToNextStep={this.goToTimeFieldStep}
/>
);
}
renderStepTwo() {
const { step, indexPattern } = this.state;
const { services } = this.props;
if (step !== 2) {
return null;
if (allIndices.length === 0) {
const { loadingDataDocUrl } = this.props;
return <EmptyState loadingDataDocUrl={loadingDataDocUrl} onRefresh={this.fetchIndices} />;
}
return (
<StepTimeField
indexPattern={indexPattern}
indexPatternsService={services.indexPatterns}
goToPreviousStep={this.goToIndexPatternStep}
createIndexPattern={this.createIndexPattern}
/>
);
if (step === 1) {
const { services, initialQuery } = this.props;
return (
<StepIndexPattern
allIndices={allIndices}
initialQuery={indexPattern || initialQuery}
isIncludingSystemIndices={isIncludingSystemIndices}
esService={services.es}
savedObjectsClient={services.savedObjectsClient}
goToNextStep={this.goToTimeFieldStep}
/>
);
}
if (step === 2) {
const { services } = this.props;
return (
<StepTimeField
indexPattern={indexPattern}
indexPatternsService={services.indexPatterns}
goToPreviousStep={this.goToIndexPatternStep}
createIndexPattern={this.createIndexPattern}
/>
);
}
return null;
}
render() {
const header = this.renderHeader();
const content = this.renderContent();
return (
<div>
{this.renderHeader()}
{this.renderInitialLoadingState()}
{this.renderInitialEmptyState()}
{this.renderStepOne()}
{this.renderStepTwo()}
{header}
{content}
</div>
);
}

View file

@ -1,10 +0,0 @@
import { createReasonableWait } from '../create_reasonable_wait';
import sinon from 'sinon';
describe('createReasonableWait', () => {
it('should eventually calls the callback', () => {
const callback = sinon.spy();
createReasonableWait(callback);
expect(callback.notCalled).toBeTruthy();
});
});

View file

@ -0,0 +1,28 @@
import { ensureMinimumTime } from '../ensure_minimum_time';
describe('ensureMinimumTime', () => {
it('resolves single promise', async (done) => {
const promiseA = new Promise(resolve => resolve('a'));
const a = await ensureMinimumTime(promiseA, 0);
expect(a).toBe('a');
done();
});
it('resolves multiple promises', async (done) => {
const promiseA = new Promise(resolve => resolve('a'));
const promiseB = new Promise(resolve => resolve('b'));
const [ a, b ] = await ensureMinimumTime([promiseA, promiseB], 0);
expect(a).toBe('a');
expect(b).toBe('b');
done();
});
it('resolves in the amount of time provided, at minimum', async (done) => {
const startTime = new Date().getTime();
const promise = new Promise(resolve => resolve());
await ensureMinimumTime(promise, 100);
const endTime = new Date().getTime();
expect(endTime - startTime).toBeGreaterThanOrEqual(100);
done();
});
});

View file

@ -1,3 +0,0 @@
export function createReasonableWait(cb) {
return setTimeout(cb, 100);
}

View file

@ -0,0 +1,32 @@
/**
* When you make an async request, typically you want to show the user a spinner while they wait.
* However, if the request takes less than 300 ms, the spinner will flicker in the UI and the user
* won't have time to register it as a spinner. This function ensures the spinner (or whatever
* you're showing the user) displays for at least 300 ms, even if the request completes before then.
*/
export const DEFAULT_MINIMUM_TIME_MS = 300;
export async function ensureMinimumTime(promiseOrPromises, minimumTimeMs = DEFAULT_MINIMUM_TIME_MS) {
let returnValue;
// Block on the async action and start the clock.
const asyncActionStartTime = new Date().getTime();
if (Array.isArray(promiseOrPromises)) {
returnValue = await Promise.all(promiseOrPromises);
} else {
returnValue = await promiseOrPromises;
}
// Measure how long the async action took to complete.
const asyncActionCompletionTime = new Date().getTime();
const asyncActionDuration = asyncActionCompletionTime - asyncActionStartTime;
// Wait longer if the async action completed too quickly.
if (asyncActionDuration < minimumTimeMs) {
const additionalWaitingTime = minimumTimeMs - (asyncActionCompletionTime - asyncActionStartTime);
await new Promise(resolve => setTimeout(resolve, additionalWaitingTime));
}
return returnValue;
}

View file

@ -1,6 +1,6 @@
export { canAppendWildcard } from './can_append_wildcard';
export { createReasonableWait } from './create_reasonable_wait';
export { ensureMinimumTime } from './ensure_minimum_time';
export { getIndices } from './get_indices';