[Management] Index pattern step in React! (#15936)

* Index pattern step in React!

* Remove dead lines

* Ensure this only shows up when applicable

* PR feedback

* Use pager

* Add tests for lib/

* PR feedback

* Tests and PR feedback

* More tests and PR feedback

* New jest functionality
This commit is contained in:
Chris Roberson 2018-01-16 11:08:40 -05:00 committed by GitHub
parent a169ac1093
commit bc3f36095f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 2144 additions and 410 deletions

View file

@ -0,0 +1,213 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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="k"
/>
<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="k"
/>
<EuiSpacer
size="s"
/>
<IndicesList
indices={
Array [
Object {
"name": "kibana",
},
Object {
"name": "es",
},
]
}
/>
</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 [
Object {
"name": "kibana",
},
],
"visibleIndices": Array [
Object {
"name": "kibana",
},
],
}
}
query="k*"
/>
<EuiSpacer
size="s"
/>
<IndicesList
indices={
Array [
Object {
"name": "kibana",
},
]
}
/>
</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 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"
/>
<StatusMessage
matchedIndices={
Object {
"allIndices": Array [
Object {
"name": "kibana",
},
Object {
"name": "es",
},
],
"exactMatchedIndices": Array [],
"partialMatchedIndices": Array [
Object {
"name": "kibana",
},
],
"visibleIndices": Array [
Object {
"name": "kibana",
},
],
}
}
query="?"
/>
<EuiSpacer
size="s"
/>
<IndicesList
indices={
Array [
Object {
"name": "kibana",
},
]
}
/>
</EuiPanel>
`;

View file

@ -0,0 +1,31 @@
const render = jest.fn();
const unmountComponentAtNode = jest.fn();
jest.doMock('react-dom', () => ({ render, unmountComponentAtNode }));
const { renderStepIndexPattern, destroyStepIndexPattern } = require('../index');
describe('StepIndexPatternRender', () => {
beforeEach(() => {
render.mockClear();
unmountComponentAtNode.mockClear();
});
it('should call render', () => {
renderStepIndexPattern(
'reactDiv',
[],
'',
false,
{},
() => {}
);
expect(render.mock.calls.length).toBe(1);
});
it('should call unmountComponentAtNode', () => {
destroyStepIndexPattern('reactDiv');
expect(unmountComponentAtNode.mock.calls.length).toBe(1);
});
});

View file

@ -0,0 +1,96 @@
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/get_indices', () => ({
getIndices: () => {
return [
{ name: 'kibana' },
];
},
}));
const allIndices = [{ name: 'kibana' }, { name: 'es' }];
const esService = {};
const goToNextStep = () => {};
describe('StepIndexPattern', () => {
it('should render normally', () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
goToNextStep={goToNextStep}
initialQuery={'k'}
/>
);
expect(component).toMatchSnapshot();
});
it('should render the loading state', () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
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}
goToNextStep={goToNextStep}
/>
);
const instance = component.instance();
await instance.onQueryChanged({
nativeEvent: { data: 'k' },
target: { value: 'k' }
});
component.update();
expect(component).toMatchSnapshot();
});
it('should show errors', async () => {
const component = shallow(
<StepIndexPattern
allIndices={allIndices}
isIncludingSystemIndices={false}
esService={esService}
goToNextStep={goToNextStep}
/>
);
const instance = component.instance();
await instance.onQueryChanged({
nativeEvent: { data: '?' },
target: { value: '?' }
});
component.update();
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,177 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header should mark the input as invalid 1`] = `
<div>
<EuiTitle
size="s"
>
<h2>
Step 1 of 2: Define index pattern
</h2>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="flexEnd"
component="div"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiForm
isInvalid={true}
>
<EuiFormRow
error={
Array [
"Input is invalid",
]
}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<div>
<p>
You can use a
<strong>
*
</strong>
as a wildcard in your index pattern.
</p>
<p>
You can't use empty spaces or the characters
<strong>
%
</strong>
.
</p>
</div>
}
isInvalid={true}
label="Index pattern"
>
<EuiFieldText
data-test-subj="createIndexPatternNameInput"
fullWidth={false}
isInvalid={true}
isLoading={false}
name="indexPattern"
onChange={[Function]}
placeholder="index-name-*"
value="%"
/>
</EuiFormRow>
</EuiForm>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
data-test-subj="createIndexPatternGoToStep2Button"
fill={false}
iconSide="left"
iconType="arrowRight"
isDisabled={true}
onClick={[Function]}
type="button"
>
Next step
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;
exports[`Header should render normally 1`] = `
<div>
<EuiTitle
size="s"
>
<h2>
Step 1 of 2: Define index pattern
</h2>
</EuiTitle>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="flexEnd"
component="div"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiForm
isInvalid={false}
>
<EuiFormRow
error={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<div>
<p>
You can use a
<strong>
*
</strong>
as a wildcard in your index pattern.
</p>
<p>
You can't use empty spaces or the characters
<strong>
%
</strong>
.
</p>
</div>
}
isInvalid={false}
label="Index pattern"
>
<EuiFieldText
data-test-subj="createIndexPatternNameInput"
fullWidth={false}
isInvalid={false}
isLoading={false}
name="indexPattern"
onChange={[Function]}
placeholder="index-name-*"
value="k"
/>
</EuiFormRow>
</EuiForm>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
data-test-subj="createIndexPatternGoToStep2Button"
fill={false}
iconSide="left"
iconType="arrowRight"
isDisabled={false}
onClick={[Function]}
type="button"
>
Next step
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;

View file

@ -0,0 +1,37 @@
import React from 'react';
import { Header } from '../header';
import { shallow } from 'enzyme';
describe('Header', () => {
it('should render normally', () => {
const component = shallow(
<Header
isInputInvalid={false}
errors={[]}
characterList={['%']}
query={'k'}
onQueryChanged={() => {}}
goToNextStep={() => {}}
isNextStepDisabled={false}
/>
);
expect(component).toMatchSnapshot();
});
it('should mark the input as invalid', () => {
const component = shallow(
<Header
isInputInvalid={true}
errors={['Input is invalid']}
characterList={['%']}
query={'%'}
onQueryChanged={() => {}}
goToNextStep={() => {}}
isNextStepDisabled={true}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,69 @@
import React from 'react';
import {
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiButton,
EuiForm,
EuiFormRow,
EuiFieldText,
} from '@elastic/eui';
export const Header = ({
isInputInvalid,
errors,
characterList,
query,
onQueryChanged,
goToNextStep,
isNextStepDisabled,
}) => (
<div>
<EuiTitle size="s">
<h2>
Step 1 of 2: Define index pattern
</h2>
</EuiTitle>
<EuiSpacer size="m"/>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="flexEnd">
<EuiFlexItem grow={false}>
<EuiForm
isInvalid={isInputInvalid}
>
<EuiFormRow
label="Index pattern"
isInvalid={isInputInvalid}
error={errors}
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>
</div>
}
>
<EuiFieldText
name="indexPattern"
placeholder="index-name-*"
value={query}
isInvalid={isInputInvalid}
onChange={onQueryChanged}
data-test-subj="createIndexPatternNameInput"
/>
</EuiFormRow>
</EuiForm>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
iconType="arrowRight"
onClick={() => goToNextStep(query)}
isDisabled={isNextStepDisabled}
data-test-subj="createIndexPatternGoToStep2Button"
>
Next step
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);

View file

@ -0,0 +1,301 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`IndicesList should change pages 1`] = `
<div>
<EuiTable>
<EuiTableBody>
<EuiTableRow
key="0"
>
<EuiTableRowCell
align="left"
textOnly={true}
>
kibana
</EuiTableRowCell>
</EuiTableRow>
<EuiTableRow
key="1"
>
<EuiTableRowCell
align="left"
textOnly={true}
>
es
</EuiTableRowCell>
</EuiTableRow>
</EuiTableBody>
</EuiTable>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="center"
component="div"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiPopover
anchorPosition="downCenter"
button={
<EuiButtonEmpty
color="text"
iconSide="right"
iconType="arrowDown"
onClick={[Function]}
size="s"
type="button"
>
Rows per page:
1
</EuiButtonEmpty>
}
closePopover={[Function]}
id="customizablePagination"
isOpen={false}
ownFocus={false}
panelPaddingSize="none"
withTitle={true}
>
<EuiContextMenuPanel
hasFocus={true}
items={
Array [
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
5
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
10
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
20
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
50
</EuiContextMenuItem>,
]
}
/>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;
exports[`IndicesList should change per page 1`] = `
<div>
<EuiTable>
<EuiTableBody>
<EuiTableRow
key="0"
>
<EuiTableRowCell
align="left"
textOnly={true}
>
kibana
</EuiTableRowCell>
</EuiTableRow>
</EuiTableBody>
</EuiTable>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="center"
component="div"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiPopover
anchorPosition="downCenter"
button={
<EuiButtonEmpty
color="text"
iconSide="right"
iconType="arrowDown"
onClick={[Function]}
size="s"
type="button"
>
Rows per page:
1
</EuiButtonEmpty>
}
closePopover={[Function]}
id="customizablePagination"
isOpen={false}
ownFocus={false}
panelPaddingSize="none"
withTitle={true}
>
<EuiContextMenuPanel
hasFocus={true}
items={
Array [
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
5
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
10
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
20
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
50
</EuiContextMenuItem>,
]
}
/>
</EuiPopover>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiPagination
activePage={0}
onPageClick={[Function]}
pageCount={2}
/>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;
exports[`IndicesList should render normally 1`] = `
<div>
<EuiTable>
<EuiTableBody>
<EuiTableRow
key="0"
>
<EuiTableRowCell
align="left"
textOnly={true}
>
kibana
</EuiTableRowCell>
</EuiTableRow>
<EuiTableRow
key="1"
>
<EuiTableRowCell
align="left"
textOnly={true}
>
es
</EuiTableRowCell>
</EuiTableRow>
</EuiTableBody>
</EuiTable>
<EuiSpacer
size="m"
/>
<EuiFlexGroup
alignItems="center"
component="div"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiPopover
anchorPosition="downCenter"
button={
<EuiButtonEmpty
color="text"
iconSide="right"
iconType="arrowDown"
onClick={[Function]}
size="s"
type="button"
>
Rows per page:
10
</EuiButtonEmpty>
}
closePopover={[Function]}
id="customizablePagination"
isOpen={false}
ownFocus={false}
panelPaddingSize="none"
withTitle={true}
>
<EuiContextMenuPanel
hasFocus={true}
items={
Array [
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
5
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
10
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
20
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="empty"
onClick={[Function]}
>
50
</EuiContextMenuItem>,
]
}
/>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
</div>
`;

View file

@ -0,0 +1,44 @@
import React from 'react';
import { IndicesList } from '../indices_list';
import { shallow } from 'enzyme';
const indices = [
{ name: 'kibana' },
{ name: 'es' }
];
describe('IndicesList', () => {
it('should render normally', () => {
const component = shallow(
<IndicesList indices={indices}/>
);
expect(component).toMatchSnapshot();
});
it('should change pages', () => {
const component = shallow(
<IndicesList indices={indices}/>
);
const instance = component.instance();
component.setState({ perPage: 1 });
instance.onChangePage(1);
component.update();
expect(component).toMatchSnapshot();
});
it('should change per page', () => {
const component = shallow(
<IndicesList indices={indices}/>
);
const instance = component.instance();
instance.onChangePerPage(1);
component.update();
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,148 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { PER_PAGE_INCREMENTS } from '../../../../constants';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTable,
EuiTableBody,
EuiTableRow,
EuiTableRowCell,
EuiButtonEmpty,
EuiContextMenuItem,
EuiContextMenuPanel,
EuiPagination,
EuiPopover,
} from '@elastic/eui';
import {
Pager
} from '@elastic/eui/lib/services';
export class IndicesList extends Component {
static propTypes = {
indices: PropTypes.array.isRequired,
}
constructor(props) {
super(props);
this.state = {
page: 0,
perPage: PER_PAGE_INCREMENTS[1],
isPerPageControlOpen: false,
};
this.pager = new Pager(props.indices.length, this.state.perPage, this.state.page);
}
onChangePage = page => {
this.pager.goToPageIndex(page);
this.setState({ page });
}
onChangePerPage = perPage => {
this.pager.setItemsPerPage(perPage);
this.setState({ perPage });
this.closePerPageControl();
}
openPerPageControl = () => {
this.setState({ isPerPageControlOpen: true });
}
closePerPageControl = () => {
this.setState({ isPerPageControlOpen: false });
}
renderPagination() {
const { perPage, page, isPerPageControlOpen } = this.state;
const button = (
<EuiButtonEmpty
size="s"
color="text"
iconType="arrowDown"
iconSide="right"
onClick={this.openPerPageControl}
>
Rows per page: {perPage}
</EuiButtonEmpty>
);
const items = PER_PAGE_INCREMENTS.map(increment => {
return (
<EuiContextMenuItem
key={increment}
icon="empty"
onClick={() => this.onChangePerPage(increment)}
>
{increment}
</EuiContextMenuItem>
);
});
const pageCount = this.pager.getTotalPages();
const paginationControls = pageCount > 1
? (
<EuiFlexItem grow={false}>
<EuiPagination
pageCount={pageCount}
activePage={page}
onPageClick={this.onChangePage}
/>
</EuiFlexItem>
)
: null;
return (
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiPopover
id="customizablePagination"
button={button}
isOpen={isPerPageControlOpen}
closePopover={this.closePerPageControl}
panelPaddingSize="none"
withTitle
>
<EuiContextMenuPanel
items={items}
/>
</EuiPopover>
</EuiFlexItem>
{paginationControls}
</EuiFlexGroup>
);
}
render() {
const { indices } = this.props;
const paginatedIndices = indices.slice(this.pager.firstItemIndex, this.pager.lastItemIndex + 1);
const rows = paginatedIndices.map((index, key) => {
return (
<EuiTableRow key={key}>
<EuiTableRowCell>
{index.name}
</EuiTableRowCell>
</EuiTableRow>
);
});
return (
<div>
<EuiTable>
<EuiTableBody>
{rows}
</EuiTableBody>
</EuiTable>
<EuiSpacer size="m"/>
{this.renderPagination()}
</div>
);
}
}

View file

@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LoadingIndices should render normally 1`] = `
<EuiFlexGroup
alignItems="center"
component="div"
gutterSize="l"
justifyContent="center"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiLoadingSpinner
size="m"
/>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiText>
<EuiTextColor
color="subdued"
>
Looking for matching indices...
</EuiTextColor>
</EuiText>
<EuiText
size="s"
style={
Object {
"textAlign": "center",
}
}
>
<EuiTextColor
color="subdued"
>
Just a sec...
</EuiTextColor>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -0,0 +1,13 @@
import React from 'react';
import { LoadingIndices } from '../loading_indices';
import { shallow } from 'enzyme';
describe('LoadingIndices', () => {
it('should render normally', () => {
const component = shallow(
<LoadingIndices/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,29 @@
import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiTextColor,
EuiLoadingSpinner,
} from '@elastic/eui';
export const LoadingIndices = () => (
<EuiFlexGroup justifyContent="center" alignItems="center">
<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...
</EuiTextColor>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -0,0 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatusMessage should render with exact matches 1`] = `
<EuiText
size="s"
>
<EuiTextColor
color="secondary"
>
<EuiIcon
size="m"
type="check"
/>
<span>
 
<strong>
Success!
</strong>
  Your index pattern matches
<strong>
1
index
</strong>
.
</span>
</EuiTextColor>
</EuiText>
`;
exports[`StatusMessage should render with no partial matches 1`] = `
<EuiText
size="s"
>
<EuiTextColor
color="default"
>
<span>
The index pattern you've entered doesn't match any indices. You can match any of your
<strong>
2
indices
</strong>
, below.
</span>
</EuiTextColor>
</EuiText>
`;
exports[`StatusMessage should render with partial matches 1`] = `
<EuiText
size="s"
>
<EuiTextColor
color="default"
>
<span>
Your index pattern doesn't match any indices, but you have 
<strong>
1
index
</strong>
which
looks
similar.
</span>
</EuiTextColor>
</EuiText>
`;
exports[`StatusMessage should render without a query 1`] = `
<EuiText
size="s"
>
<EuiTextColor
color="default"
>
<span>
Your index pattern can match any of your
<strong>
2
indices
</strong>
, below.
</span>
</EuiTextColor>
</EuiText>
`;

View file

@ -0,0 +1,72 @@
import React from 'react';
import { StatusMessage } from '../status_message';
import { shallow } from 'enzyme';
const matchedIndices = {
allIndices: [
{ name: 'kibana' },
{ name: 'es' }
],
exactMatchedIndices: [],
partialMatchedIndices: [
{ name: 'kibana' }
],
};
describe('StatusMessage', () => {
it('should render without a query', () => {
const component = shallow(
<StatusMessage
matchedIndices={matchedIndices}
query={''}
/>
);
expect(component).toMatchSnapshot();
});
it('should render with exact matches', () => {
const localMatchedIndices = {
...matchedIndices,
exactMatchedIndices: [
{ name: 'kibana' }
]
};
const component = shallow(
<StatusMessage
matchedIndices={localMatchedIndices}
query={'k*'}
/>
);
expect(component).toMatchSnapshot();
});
it('should render with partial matches', () => {
const component = shallow(
<StatusMessage
matchedIndices={matchedIndices}
query={'k'}
/>
);
expect(component).toMatchSnapshot();
});
it('should render with no partial matches', () => {
const localMatchedIndices = {
...matchedIndices,
partialMatchedIndices: []
};
const component = shallow(
<StatusMessage
matchedIndices={localMatchedIndices}
query={'k'}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,76 @@
import React from 'react';
import {
EuiText,
EuiTextColor,
EuiIcon,
} from '@elastic/eui';
export const StatusMessage = ({
matchedIndices: {
allIndices,
exactMatchedIndices,
partialMatchedIndices
},
query,
}) => {
let statusIcon;
let statusMessage;
let statusColor;
if (query.length === 0) {
statusIcon = null;
statusColor = 'default';
statusMessage = allIndices.length > 1
? (
<span>
Your index pattern can match any of your <strong>{allIndices.length} indices</strong>, below.
</span>
)
: (<span>You only have a single index. You can create an index pattern to match it.</span>);
}
else if (exactMatchedIndices.length) {
statusIcon = 'check';
statusColor = 'secondary';
statusMessage = (
<span>
&nbsp;
<strong>Success!</strong>
&nbsp;
Your index pattern matches <strong>{exactMatchedIndices.length} {exactMatchedIndices.length > 1 ? 'indices' : 'index'}</strong>.
</span>
);
}
else if (partialMatchedIndices.length) {
statusIcon = null;
statusColor = 'default';
statusMessage = (
<span>
Your index pattern doesn&apos;t match any indices, but you have&nbsp;
<strong>
{partialMatchedIndices.length} {partialMatchedIndices.length > 1 ? 'indices ' : 'index '}
</strong>
which {partialMatchedIndices.length > 1 ? 'look' : 'looks'} similar.
</span>
);
}
else if (allIndices.length) {
statusIcon = null;
statusColor = 'default';
statusMessage = (
<span>
The index pattern you&apos;ve entered doesn&apos;t match any indices.
You can match any of your <strong>{allIndices.length} indices</strong>, below.
</span>
);
}
return (
<EuiText size="s">
<EuiTextColor color={statusColor}>
{ statusIcon ? <EuiIcon type={statusIcon}/> : null }
{statusMessage}
</EuiTextColor>
</EuiText>
);
};

View file

@ -0,0 +1,27 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { StepIndexPattern } from './step_index_pattern';
export function renderStepIndexPattern(
domElementId,
allIndices,
initialQuery,
isIncludingSystemIndices,
esService,
goToNextStep,
) {
render(
<StepIndexPattern
allIndices={allIndices}
initialQuery={initialQuery}
isIncludingSystemIndices={isIncludingSystemIndices}
esService={esService}
goToNextStep={goToNextStep}
/>,
document.getElementById(domElementId),
);
}
export function destroyStepIndexPattern(domElementId) {
unmountComponentAtNode(document.getElementById(domElementId));
}

View file

@ -0,0 +1,165 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ILLEGAL_CHARACTERS } from '../../constants';
import {
getIndices,
isIndexPatternQueryValid,
getMatchedIndices,
canAppendWildcard,
createReasonableWait
} from '../../lib';
import { LoadingIndices } from './components/loading_indices';
import { StatusMessage } from './components/status_message';
import { IndicesList } from './components/indices_list';
import { Header } from './components/header';
import {
EuiPanel,
EuiSpacer,
} from '@elastic/eui';
export class StepIndexPattern extends Component {
static propTypes = {
allIndices: PropTypes.array.isRequired,
isIncludingSystemIndices: PropTypes.bool.isRequired,
esService: PropTypes.object.isRequired,
goToNextStep: PropTypes.func.isRequired,
initialQuery: PropTypes.string,
}
static defaultProps = {
initialQuery: '',
}
constructor(props) {
super(props);
this.state = {
partialMatchedIndices: [],
isLoadingIndices: false,
query: props.initialQuery,
appendedWildcard: false,
showingIndexPatternQueryErrors: false,
};
}
fetchIndices = async (query) => {
const { esService } = this.props;
this.setState({ isLoadingIndices: true });
const partialMatchedIndices = await getIndices(esService, `${query}*`);
createReasonableWait(() => this.setState({ partialMatchedIndices, isLoadingIndices: false }));
}
onQueryChanged = (e) => {
const { appendedWildcard } = this.state;
const { target } = e;
let query = target.value;
if (query.length === 1 && canAppendWildcard(e.nativeEvent.data)) {
query += '*';
this.setState({ appendedWildcard: true });
setTimeout(() => target.setSelectionRange(1, 1));
}
else {
if (query === '*' && appendedWildcard) {
query = '';
this.setState({ appendedWildcard: false });
}
}
this.setState({ query, showingIndexPatternQueryErrors: !!query.length });
this.fetchIndices(query);
}
renderLoadingState() {
const { isLoadingIndices } = this.state;
if (!isLoadingIndices) {
return null;
}
return (
<LoadingIndices/>
);
}
renderStatusMessage(matchedIndices) {
const { query, isLoadingIndices } = this.state;
if (isLoadingIndices) {
return null;
}
return (
<StatusMessage
matchedIndices={matchedIndices}
query={query}
/>
);
}
renderList({ visibleIndices, allIndices }) {
const { query, isLoadingIndices } = this.state;
if (isLoadingIndices) {
return null;
}
const indicesToList = query.length
? visibleIndices
: allIndices;
return (
<IndicesList
indices={indicesToList}
/>
);
}
renderHeader({ exactMatchedIndices: indices }) {
const { goToNextStep } = this.props;
const { query, showingIndexPatternQueryErrors } = this.state;
let containsErrors = false;
const errors = [];
const characterList = ILLEGAL_CHARACTERS.slice(0, ILLEGAL_CHARACTERS.length - 1).join(', ');
if (!isIndexPatternQueryValid(query, ILLEGAL_CHARACTERS)) {
errors.push(`Your input contains invalid characters or spaces. Please omit: ${characterList}`);
containsErrors = true;
}
const isInputInvalid = showingIndexPatternQueryErrors && containsErrors;
const isNextStepDisabled = containsErrors || indices.length === 0;
return (
<Header
isInputInvalid={isInputInvalid}
errors={errors}
characterList={characterList}
query={query}
onQueryChanged={this.onQueryChanged}
goToNextStep={goToNextStep}
isNextStepDisabled={isNextStepDisabled}
/>
);
}
render() {
const { isIncludingSystemIndices, allIndices } = this.props;
const { query, partialMatchedIndices } = this.state;
const matchedIndices = getMatchedIndices(allIndices, partialMatchedIndices, query, isIncludingSystemIndices);
return (
<EuiPanel paddingSize="l">
{this.renderHeader(matchedIndices)}
<EuiSpacer size="s"/>
{this.renderLoadingState(matchedIndices)}
{this.renderStatusMessage(matchedIndices)}
<EuiSpacer size="s"/>
{this.renderList(matchedIndices)}
</EuiPanel>
);
}
}

View file

@ -0,0 +1,10 @@
// This isn't ideal. We want to avoid searching for 20 indices
// then filtering out the majority of them because they are sysetm indices.
// We'd like to filter system indices out in the query
// so if we can accomplish that in the future, this logic can go away
export const ESTIMATED_NUMBER_OF_SYSTEM_INDICES = 100;
export const MAX_NUMBER_OF_MATCHING_INDICES = 100;
export const MAX_SEARCH_SIZE = MAX_NUMBER_OF_MATCHING_INDICES + ESTIMATED_NUMBER_OF_SYSTEM_INDICES;
export const PER_PAGE_INCREMENTS = [5, 10, 20, 50];
export const ILLEGAL_CHARACTERS = ['\\', '/', '?', '"', '<', '>', '|', ' '];

View file

@ -49,7 +49,11 @@
<div class="euiSpacer euiSpacer--m"></div>
<div class="euiPanel euiPanel--paddingLarge">
<div
ng-class="{
'euiPanel euiPanel--paddingLarge': !controller.hasIndices() || controller.wizardStep !== 'indexPattern'
}"
>
<!-- User has no data -->
<div ng-if="!controller.hasIndices()">
<div class="euiPageContentBody">
@ -125,7 +129,10 @@
ng-switch="controller.wizardStep"
>
<!-- Specify index pattern -->
<step-index-pattern
<div ng-switch-when="indexPattern">
<div id="stepIndexPatternReact"></div>
</div>
<!-- <step-index-pattern
ng-switch-when="indexPattern"
fetch-existing-indices="controller.fetchExistingIndices()"
is-fetching-existing-indices="controller.isFetchingExistingIndices"
@ -137,7 +144,7 @@
partial-matching-indices="controller.partialMatchingIndices"
matching-indices="controller.matchingIndices"
go-to-next-step="controller.goToTimeFieldStep()"
></step-index-pattern>
></step-index-pattern> -->
<!-- Specify optional time field -->
<step-time-field

View file

@ -7,7 +7,7 @@ import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import template from './create_index_pattern_wizard.html';
import { sendCreateIndexPatternRequest } from './send_create_index_pattern_request';
import './step_index_pattern';
import { renderStepIndexPattern, destroyStepIndexPattern } from './components/step_index_pattern';
import './step_time_field';
import './matching_indices_list';
import './create_index_pattern_wizard.less';
@ -33,7 +33,7 @@ uiModules.get('apps/management')
// then filtering out the majority of them because they are sysetm indices.
// We'd like to filter system indices out in the query
// so if we can accomplish that in the future, this logic can go away
const ESTIMATED_NUMBER_OF_SYSTEM_INDICES = 20;
const ESTIMATED_NUMBER_OF_SYSTEM_INDICES = 100;
const MAX_NUMBER_OF_MATCHING_INDICES = 20;
const MAX_SEARCH_SIZE = MAX_NUMBER_OF_MATCHING_INDICES + ESTIMATED_NUMBER_OF_SYSTEM_INDICES;
const notify = new Notifier();
@ -45,6 +45,12 @@ uiModules.get('apps/management')
display: `I don't want to use the Time Filter`,
};
const REACT_DOM_ELEMENT_ID = 'stepIndexPatternReact';
$scope.$on('$destroy', () => {
destroyStepIndexPattern(REACT_DOM_ELEMENT_ID);
});
// Configure the new index pattern we're going to create.
this.formValues = {
id: $routeParams.id ? decodeURIComponent($routeParams.id) : undefined,
@ -62,8 +68,8 @@ uiModules.get('apps/management')
this.isCreatingIndexPattern = false;
this.doesIncludeSystemIndices = false;
let allIndices = [];
let matchingIndices = [];
let partialMatchingIndices = [];
const matchingIndices = [];
const partialMatchingIndices = [];
this.allIndices = [];
this.matchingIndices = [];
this.partialMatchingIndices = [];
@ -149,42 +155,7 @@ uiModules.get('apps/management')
this.onIncludeSystemIndicesChange = () => {
updateWhiteListedIndices();
};
let mostRecentFetchMatchingIndicesRequest;
this.fetchMatchingIndices = () => {
this.isFetchingMatchingIndices = true;
// Default to searching for all indices.
const exactSearchQuery = this.formValues.name;
let partialSearchQuery = this.formValues.name;
if (!_.endsWith(partialSearchQuery, '*')) {
partialSearchQuery = `${partialSearchQuery}*`;
}
if (!_.startsWith(partialSearchQuery, '*')) {
partialSearchQuery = `*${partialSearchQuery}`;
}
const thisFetchMatchingIndicesRequest = mostRecentFetchMatchingIndicesRequest = Promise.all([
getIndices(exactSearchQuery),
getIndices(partialSearchQuery),
createReasonableWait()
])
.then(([
matchingIndicesResponse,
partialMatchingIndicesResponse
]) => {
if (thisFetchMatchingIndicesRequest === mostRecentFetchMatchingIndicesRequest) {
matchingIndices = matchingIndicesResponse;
partialMatchingIndices = partialMatchingIndicesResponse;
updateWhiteListedIndices();
this.isFetchingMatchingIndices = false;
}
}).catch(error => {
notify.error(error);
});
this.renderStepIndexPatternReact();
};
this.fetchExistingIndices = () => {
@ -200,6 +171,9 @@ uiModules.get('apps/management')
allIndices = allIndicesResponse;
updateWhiteListedIndices();
this.isFetchingExistingIndices = false;
if (allIndices.length) {
this.renderStepIndexPatternReact();
}
}).catch(error => {
notify.error(error);
this.isFetchingExistingIndices = false;
@ -212,6 +186,23 @@ uiModules.get('apps/management')
this.goToIndexPatternStep = () => {
this.wizardStep = 'indexPattern';
this.renderStepIndexPatternReact();
};
this.renderStepIndexPatternReact = () => {
$scope.$$postDigest(() => renderStepIndexPattern(
REACT_DOM_ELEMENT_ID,
allIndices,
this.formValues.name,
this.doesIncludeSystemIndices,
es,
query => {
destroyStepIndexPattern(REACT_DOM_ELEMENT_ID);
this.formValues.name = query;
this.goToTimeFieldStep();
$scope.$apply();
}
));
};
this.goToTimeFieldStep = () => {

View file

@ -0,0 +1,4 @@
{
"statusCode": 400,
"error": "Bad Request"
}

View file

@ -0,0 +1,27 @@
{
"body": {
"error": {
"root_cause": [
{
"type": "index_not_found_exception",
"reason": "no such index",
"index_uuid": "_na_",
"resource.type": "index_or_alias",
"resource.id": "t",
"index": "t"
}
],
"type": "transport_exception",
"reason": "unable to communicate with remote cluster [cluster_one]",
"caused_by": {
"type": "index_not_found_exception",
"reason": "no such index",
"index_uuid": "_na_",
"resource.type": "index_or_alias",
"resource.id": "t",
"index": "t"
}
}
},
"status": 500
}

View file

@ -0,0 +1,20 @@
{
"hits": {
"total": 1,
"max_score": 0.0,
"hits": []
},
"aggregations": {
"indices": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [{
"key": "1",
"doc_count": 1
},{
"key": "2",
"doc_count": 1
}]
}
}
}

View file

@ -0,0 +1,27 @@
import { canAppendWildcard } from '../can_append_wildcard';
describe('canAppendWildcard', () => {
test('ignores no data', () => {
expect(canAppendWildcard({})).toBeFalsy();
});
test('ignores symbols', () => {
expect(canAppendWildcard('%')).toBeFalsy();
});
test('accepts numbers', () => {
expect(canAppendWildcard('1')).toBeTruthy();
});
test('accepts letters', () => {
expect(canAppendWildcard('b')).toBeTruthy();
});
test('accepts uppercase letters', () => {
expect(canAppendWildcard('B')).toBeTruthy();
});
test('ignores if more than one key pressed', () => {
expect(canAppendWildcard('ab')).toBeFalsy();
});
});

View file

@ -0,0 +1,10 @@
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,73 @@
import { getIndices } from '../get_indices';
import successfulResponse from './api/get_indices.success.json';
import errorResponse from './api/get_indices.error.json';
import exceptionResponse from './api/get_indices.exception.json';
describe('getIndices', () => {
it('should work in a basic case', async () => {
const es = {
search: () => new Promise((resolve) => resolve(successfulResponse))
};
const result = await getIndices(es, 'kibana', 1);
expect(result.length).toBe(2);
expect(result[0].name).toBe('1');
expect(result[1].name).toBe('2');
});
it('should ignore ccs query-all', async () => {
expect((await getIndices(null, '*:')).length).toBe(0);
});
it('should trim the input', async () => {
let index;
const es = {
search: jest.fn().mockImplementation(params => {
index = params.index;
}),
};
await getIndices(es, 'kibana ', 1);
expect(index).toBe('kibana');
});
it('should use the limit', async () => {
let limit;
const es = {
search: jest.fn().mockImplementation(params => {
limit = params.body.aggs.indices.terms.size;
}),
};
await getIndices(es, 'kibana', 10);
expect(limit).toBe(10);
});
describe('errors', () => {
it('should handle errors gracefully', async () => {
const es = {
search: () => new Promise((resolve) => resolve(errorResponse))
};
const result = await getIndices(es, 'kibana', 1);
expect(result.length).toBe(0);
});
it('should throw exceptions', async () => {
const es = {
search: () => { throw 'Fail'; }
};
await expect(getIndices(es, 'kibana', 1)).rejects.toThrow();
});
it('should handle index_not_found_exception errors gracefully', async () => {
const es = {
search: () => new Promise((resolve, reject) => reject(exceptionResponse))
};
const result = await getIndices(es, 'kibana', 1);
expect(result.length).toBe(0);
});
});
});

View file

@ -0,0 +1,126 @@
import { getMatchedIndices } from '../get_matched_indices';
jest.mock('../../constants', () => ({
MAX_NUMBER_OF_MATCHING_INDICES: 5,
}));
describe('getMatchedIndices', () => {
it('should return exact matches if they exist', () => {
const indices = [
{ name: 'kibana' },
{ name: 'es' },
{ name: '.kibana' }
];
const query = 'kibana';
const matchedIndices = [
{ name: '.kibana' },
{ name: 'kibana' },
];
const result = getMatchedIndices(indices, matchedIndices, query, false);
expect(result).toEqual({
allIndices: [{ name: 'kibana' }, { name: 'es' }],
exactMatchedIndices: [{ name: 'kibana' }],
partialMatchedIndices: [{ name: 'kibana' }],
visibleIndices: [{ name: 'kibana' }]
});
});
it('should support queries with wildcards', () => {
const indices = [
{ name: 'kibana' },
{ name: 'es' },
{ name: '.kibana' }
];
const query = 'ki*';
const matchedIndices = [
{ name: '.kibana' },
{ name: 'kibana' },
];
const result = getMatchedIndices(indices, matchedIndices, query, false);
expect(result).toEqual({
allIndices: [{ name: 'kibana' }, { name: 'es' }],
exactMatchedIndices: [{ name: 'kibana' }],
partialMatchedIndices: [{ name: 'kibana' }],
visibleIndices: [{ name: 'kibana' }]
});
});
it('should return all indices as visible if there are no partial or exact matches', () => {
const indices = [
{ name: 'kibana' },
{ name: 'es' },
{ name: '.kibana' }
];
const query = 'fo';
const matchedIndices = [];
const result = getMatchedIndices(indices, matchedIndices, query, false);
expect(result).toEqual({
allIndices: [{ name: 'kibana' }, { name: 'es' }],
exactMatchedIndices: [],
partialMatchedIndices: [],
visibleIndices: [{ name: 'kibana' }, { name: 'es' }]
});
});
it('should support showing system indices', () => {
const indices = [
{ name: 'kibana' },
{ name: 'es' },
{ name: '.kibana' }
];
const query = 'ki';
const matchedIndices = [
{ name: '.kibana' },
{ name: 'kibana' },
];
const result = getMatchedIndices(indices, matchedIndices, query, true);
expect(result).toEqual({
allIndices: [{ name: 'kibana' }, { name: 'es' }, { name: '.kibana' }],
exactMatchedIndices: [],
partialMatchedIndices: [{ name: '.kibana' }, { name: 'kibana' }],
visibleIndices: [{ name: '.kibana' }, { name: 'kibana' }]
});
});
it('should only return the max number of indices', () => {
const indices = [
{ name: 'kibana' },
{ name: 'es' },
{ name: '.kibana' },
{ name: 'monitor' },
{ name: '.monitor' },
{ name: 'metricbeat' },
];
const query = '';
const matchedIndices = [
{ name: 'kibana' },
{ name: 'es' },
{ name: '.kibana' },
{ name: 'monitor' },
{ name: '.monitor' },
{ name: 'metricbeat' },
];
const result = getMatchedIndices(indices, matchedIndices, query, true);
expect(result).toEqual({
allIndices: [{ name: 'kibana' }, { name: 'es' }, { name: '.kibana' }, { name: 'monitor' }, { name: '.monitor' }],
exactMatchedIndices: [],
partialMatchedIndices: [{ name: 'kibana' }, { name: 'es' }, { name: '.kibana' }, { name: 'monitor' }, { name: '.monitor' }],
visibleIndices: [{ name: 'kibana' }, { name: 'es' }, { name: '.kibana' }, { name: 'monitor' }, { name: '.monitor' }]
});
});
});

View file

@ -0,0 +1,33 @@
import { isIndexPatternQueryValid } from '../is_index_pattern_query_valid';
describe('isIndexPatternQueryValid', () => {
it('should fail with illegal characters', () => {
const valid = isIndexPatternQueryValid('abc', ['a']);
expect(valid).toBeFalsy();
});
it('should pass with no illegal characters', () => {
const valid = isIndexPatternQueryValid('abc', ['%']);
expect(valid).toBeTruthy();
});
it('should fail if the pattern starts with a single dot', () => {
const valid = isIndexPatternQueryValid('.');
expect(valid).toBeFalsy();
});
it('should fail if the pattern starts with a double dot', () => {
const valid = isIndexPatternQueryValid('..');
expect(valid).toBeFalsy();
});
it('should fail if no pattern is passed in', () => {
const valid = isIndexPatternQueryValid(null);
expect(valid).toBeFalsy();
});
it('should fail if an empty pattern is passed in', () => {
const valid = isIndexPatternQueryValid('');
expect(valid).toBeFalsy();
});
});

View file

@ -0,0 +1,7 @@
export const canAppendWildcard = (keyPressed) => {
// If it's not a letter, number or is something longer, reject it
if (!keyPressed || !/[a-z0-9]/i.test(keyPressed) || keyPressed.length !== 1) {
return false;
}
return true;
};

View file

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

View file

@ -0,0 +1,51 @@
import { get, sortBy } from 'lodash';
export async function getIndices(es, rawPattern, limit) {
const pattern = rawPattern.trim();
// Searching for `*:` fails for CCS environments. The search request
// is worthless anyways as the we should only send a request
// for a specific query (where we do not append *) if there is at
// least a single character being searched for.
if (pattern === '*:') {
return [];
}
const params = {
index: pattern,
ignore: [404],
body: {
size: 0, // no hits
aggs: {
indices: {
terms: {
field: '_index',
size: limit,
}
}
}
}
};
try {
const response = await es.search(params);
if (!response || response.error || !response.aggregations) {
return [];
}
return sortBy(response.aggregations.indices.buckets.map(bucket => {
return {
name: bucket.key
};
}), 'name');
}
catch (err) {
const type = get(err, 'body.error.caused_by.type');
if (type === 'index_not_found_exception') {
// This happens in a CSS environment when the controlling node returns a 500 even though the data
// nodes returned a 404. Remove this when/if this is handled: https://github.com/elastic/elasticsearch/issues/27461
return [];
}
throw err;
}
}

View file

@ -0,0 +1,53 @@
import { MAX_NUMBER_OF_MATCHING_INDICES } from '../constants';
function filterSystemIndices(indices, isIncludingSystemIndices) {
if (!indices) {
return indices;
}
const acceptableIndices = isIncludingSystemIndices
? indices
// All system indices begin with a period.
: indices.filter(index => !index.name.startsWith('.'));
return acceptableIndices.slice(0, MAX_NUMBER_OF_MATCHING_INDICES);
}
export function getMatchedIndices(
unfilteredAllIndices,
unfilteredPartialMatchedIndices,
query,
isIncludingSystemIndices
) {
const allIndices = filterSystemIndices(unfilteredAllIndices, isIncludingSystemIndices);
const partialMatchedIndices = filterSystemIndices(unfilteredPartialMatchedIndices, isIncludingSystemIndices);
const exactIndices = partialMatchedIndices.filter(({ name }) => {
if (name === query) {
return true;
}
if (query.endsWith('*') && name.indexOf(query.substring(0, query.length - 1)) === 0) {
return true;
}
return false;
});
const exactMatchedIndices = filterSystemIndices(exactIndices, isIncludingSystemIndices);
let visibleIndices;
if (exactMatchedIndices.length) {
visibleIndices = exactMatchedIndices;
}
else if (partialMatchedIndices.length) {
visibleIndices = partialMatchedIndices;
}
else {
visibleIndices = allIndices;
}
return {
allIndices,
exactMatchedIndices,
partialMatchedIndices,
visibleIndices,
};
}

View file

@ -0,0 +1,9 @@
export { canAppendWildcard } from './can_append_wildcard';
export { createReasonableWait } from './create_reasonable_wait';
export { getIndices } from './get_indices';
export { getMatchedIndices } from './get_matched_indices';
export { isIndexPatternQueryValid } from './is_index_pattern_query_valid';

View file

@ -0,0 +1,11 @@
export function isIndexPatternQueryValid(pattern, illegalCharacters) {
if (!pattern || !pattern.length) {
return false;
}
if (pattern === '.' || pattern === '..') {
return false;
}
return !illegalCharacters.some(char => pattern.includes(char));
}

View file

@ -1,29 +0,0 @@
import { uiModules } from 'ui/modules';
import { appendWildcard } from './lib/append_wildcard';
const module = uiModules.get('apps/management');
/**
* This directive automatically appends a wildcard to the input field
* after the user starts typing. It lets the user delete the wildcard
* if necessary. If the value of the input field is set back to an empty
* string, the wildcard is immediately re-appended after the user starts
* typing. This is intended to be a UX improvement for the index pattern
* creation page. See https://github.com/elastic/kibana/pull/13454
*/
module.directive('appendWildcard', function () {
return {
require: 'ngModel',
link: function ($scope, $elem, $attrs, $ctrl) {
$elem.on('keydown', (e) => {
const newIndexPattern = appendWildcard(e, $elem.val());
if (newIndexPattern) {
e.preventDefault();
$elem.val(newIndexPattern);
$elem[0].setSelectionRange(1, 1);
$ctrl.$setViewValue(newIndexPattern);
}
});
}
};
});

View file

@ -1,40 +0,0 @@
import expect from 'expect.js';
import { appendWildcard } from '../append_wildcard';
describe('append_wildcard', function () {
it('should add a wildcard for an alphabet input', () => {
[
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
].forEach(char => {
expect(appendWildcard({ key: char }, '')).to.be(`${char}*`);
expect(appendWildcard({ key: char.toUpperCase() }, '')).to.be(`${char.toUpperCase()}*`);
});
});
it('should add a wildcard for a number input', () => {
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].forEach(num => {
expect(appendWildcard({ key: num }, '')).to.be(`${num}*`);
});
});
it('should NOT add a wildcard for a non alphanumeric input', () => {
['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+'].forEach(char => {
expect(appendWildcard({ key: char }, '')).to.be(undefined);
});
});
it('should NOT add a wildcard for multi-length input', () => {
expect(appendWildcard({ key: 'Tab' }, '')).to.be(undefined);
});
it('should NOT add a wildcard if the value is longer than 1 character', () => {
expect(appendWildcard({ key: 'a' }, 'b')).to.be(undefined);
});
it('should NOT add a wildcard if the input is a wildcard', () => {
expect(appendWildcard({ key: '*' }, '')).to.be(undefined);
});
});

View file

@ -1,35 +0,0 @@
export const appendWildcard = (keyboardEvent, value) => {
const {
key: keyPressed,
metaKey,
ctrlKey,
altKey,
} = keyboardEvent;
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
// is not recommended so we need to rely on `key` but browser support
// is still spotty (https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
// so just bail if it's not supported
if (!keyPressed) {
return;
}
// If the user is holding down ctrl/cmd, they are performing some shortcut
// and do not interpret literally
if (metaKey || ctrlKey || altKey) {
return;
}
// If it's not a letter, number or is something longer, reject it
if (!/[a-z0-9]/i.test(keyPressed) || keyPressed.length !== 1) {
return;
}
let newValue = value + keyPressed;
if (newValue.length !== 1 || newValue === '*') {
return;
}
newValue += '*';
return newValue;
};

View file

@ -1,165 +0,0 @@
<div>
<h2 class="euiTitle euiTitle--small">
Step 1 of 2: Define index pattern
</h2>
<div class="euiSpacer euiSpacer--m"></div>
<form
name="stepIndexPattern.indexPatternNameForm"
role="form"
ng-submit="stepIndexPattern.goToNextStep()"
>
<div class="euiFlexGroup euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--alignItemsFlexEnd euiFlexGroup--responsive">
<div class="euiFlexItem euiFlexItem--flexGrowZero">
<div class="euiFormRow">
<label
for="indexPatternNameField"
class="euiFormLabel"
>
Index pattern
</label>
<div class="euiFormControlLayout">
<!-- Index pattern input -->
<input
id="indexPatternNameField"
class="euiFieldText euiFieldText--fullWidth createIndexPatternInputField"
data-test-subj="createIndexPatternNameInput"
ng-model="stepIndexPattern.indexPatternName"
placeholder="index-name-*"
validate-index-pattern
validate-index-pattern-allow-wildcard
append-wildcard
name="name"
required
type="text"
aria-describedby="indexPatternNameFieldHelp1 indexPatternNameFieldHelp2"
>
</div>
<div class="euiFormHelpText euiFormRow__text">
<p
id="indexPatternNameFieldHelp1"
class="euiTextColor euiTextColor--subdued"
>
You can use a <strong>*</strong> as a wildcard in your index pattern.
</p>
<div class="euiSpacer euiSpacer--xs"></div>
<p
id="indexPatternNameFieldHelp2"
class="euiTextColor euiTextColor--subdued"
>
You can't use empty spaces or the characters <strong>\ / ? " < > |</strong>.
</p>
</div>
</div>
</div>
<div class="euiFlexItem euiFlexItem--flexGrowZero">
<!-- Action -->
<button
data-test-subj="createIndexPatternGoToStep2Button"
class="euiButton euiButton--primary"
ng-click="stepIndexPattern.goToNextStep()"
ng-disabled="!stepIndexPattern.canGoToNextStep()"
>
<span class="euiButton__content">
Next step
</span>
</button>
</div>
</div>
</form>
<div class="euiSpacer euiSpacer--m"></div>
<!-- List of matching indices -->
<div ng-switch="stepIndexPattern.matchingIndicesListType">
<div ng-switch-when="invalidIndexPattern">
<matching-indices-list
is-loading="stepIndexPattern.isFetchingMatchingIndices"
indices="stepIndexPattern.allIndices"
pattern="stepIndexPattern.indexPatternName"
>
<span class="euiTextColor euiTextColor--danger">
<span aria-hidden="true">
<svg class="euiIcon euiIcon--medium" xmlns="http://www.w3.org/2000/svg"
width="16" height="16" viewBox="0 0 16 16">
<g fill="#13252D" fill-rule="evenodd">
<circle cx="5" cy="5" r="1" />
<circle cx="10" cy="5" r="1" />
<path fill-rule="nonzero" d="M7.5 14a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13zm0 1a7.5 7.5 0 1 1 0-15 7.5 7.5 0 0 1 0 15z"
/>
<path fill-rule="nonzero" d="M12.332 9.626C10.747 8.217 9.133 7.5 7.5 7.5s-3.247.717-4.832 2.126a.5.5 0 1 0 .664.748C4.747 9.116 6.133 8.5 7.5 8.5s2.753.616 4.168 1.874a.5.5 0 0 0 .664-.748z"
/>
</g>
</svg>
</span>
<span>
You've entered an invalid index pattern. Please adjust it to match any of your <strong>{{stepIndexPattern.allIndices.length}} indices</strong>, below.
</span>
</span>
</matching-indices-list>
</div>
<div ng-switch-when="noInput">
<matching-indices-list
is-loading="stepIndexPattern.isFetchingMatchingIndices"
indices="stepIndexPattern.allIndices"
pattern="stepIndexPattern.indexPatternName"
>
<span ng-if="!stepIndexPattern.allIndices.length">
You only have a single index. You can create an index pattern to match it.
</span>
<span ng-if="stepIndexPattern.allIndices.length">
Your index pattern can match any of your <strong>{{stepIndexPattern.allIndices.length}} indices</strong>, below.
</span>
</matching-indices-list>
</div>
<div ng-switch-when="noMatches">
<matching-indices-list
is-loading="stepIndexPattern.isFetchingMatchingIndices"
indices="stepIndexPattern.allIndices"
pattern="stepIndexPattern.indexPatternName"
>
<span>
The index pattern you've entered doesn't match any indices. You can match any of your <strong>{{stepIndexPattern.allIndices.length}} indices</strong>, below.
</span>
</matching-indices-list>
</div>
<div ng-switch-when="partialMatches">
<matching-indices-list
is-loading="stepIndexPattern.isFetchingMatchingIndices"
indices="stepIndexPattern.partialMatchingIndices"
pattern="stepIndexPattern.indexPatternName"
>
<span>
Your index pattern doesn't match any indices, but you have <strong>{{stepIndexPattern.partialMatchingIndices.length}} {{stepIndexPattern.partialMatchingIndices.length > 1 ? 'indices' : 'index'}}</strong> which {{stepIndexPattern.partialMatchingIndices.length > 1 ? 'look' : 'looks'}} similar.
</span>
</matching-indices-list>
</div>
<div ng-switch-when="exactMatches">
<matching-indices-list
is-loading="stepIndexPattern.isFetchingMatchingIndices"
indices="stepIndexPattern.matchingIndices"
pattern="stepIndexPattern.indexPatternName"
>
<span class="euiTextColor euiTextColor--secondary">
<span aria-hidden="true">
<svg class="euiIcon euiIcon--medium" xmlns="http://www.w3.org/2000/svg"
xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<path id="check-a" d="M6.5 12a.502.502 0 0 1-.354-.146l-4-4a.502.502 0 0 1 .708-.708L6.5 10.793l6.646-6.647a.502.502 0 0 1 .708.708l-7 7A.502.502 0 0 1 6.5 12"
/>
</defs>
<use href="#check-a" />
</svg>
</span>
<span>
<strong>Success!</strong> Your index pattern matches <strong>{{stepIndexPattern.matchingIndices.length}} {{stepIndexPattern.matchingIndices.length > 1 ? 'indices' : 'index'}}</strong>.
</span>
</span>
</matching-indices-list>
</div>
</div>
</div>

View file

@ -1,94 +0,0 @@
import { uiModules } from 'ui/modules';
import './step_index_pattern.less';
import template from './step_index_pattern.html';
import './append_wildcard';
const module = uiModules.get('apps/management');
module.directive('stepIndexPattern', function () {
return {
restrict: 'E',
template,
replace: true,
controllerAs: 'stepIndexPattern',
bindToController: true,
scope: {
fetchExistingIndices: '&',
isFetchingExistingIndices: '=',
fetchMatchingIndices: '&',
isFetchingMatchingIndices: '=',
hasIndices: '&',
indexPatternName: '=',
allIndices: '=',
partialMatchingIndices: '=',
matchingIndices: '=',
goToNextStep: '&',
},
link: function (scope) {
scope.$watch('stepIndexPattern.allIndices', scope.stepIndexPattern.updateList);
scope.$watch('stepIndexPattern.matchingIndices', scope.stepIndexPattern.updateList);
scope.$watch('stepIndexPattern.indexPatternName', () => {
// Only send the request if there's valid input.
if (scope.stepIndexPattern.indexPatternNameForm && scope.stepIndexPattern.indexPatternNameForm.$valid) {
scope.stepIndexPattern.fetchMatchingIndices();
}
// If the index pattern name is invalid, we should reflect that state in the list.
scope.stepIndexPattern.updateList();
});
scope.$watchCollection('stepIndexPattern.indexPatternNameForm.$error', () => {
// If we immediately replace the input with an invalid string, then only the form state
// changes, but not the `indexPatternName` value, so we need to watch both.
scope.stepIndexPattern.updateList();
});
},
controller: function () {
this.matchingIndicesListType = 'noMatches';
this.canGoToNextStep = () => (
!this.isFetchingMatchingIndices
&& !this.indexPatternNameForm.$invalid
&& this.hasExactMatches()
);
const hasInvalidIndexPattern = () => (
this.indexPatternNameForm
&& !this.indexPatternNameForm.$error.required
&& this.indexPatternNameForm.$error.indexPattern
);
const hasNoInput = () => (
!this.indexPatternName
|| !this.indexPatternName.trim()
);
this.hasExactMatches = () => (
this.matchingIndices.length
);
const hasPartialMatches = () => (
!this.matchingIndices.length
&& this.partialMatchingIndices.length
);
this.updateList = () => {
if (hasInvalidIndexPattern()) {
return this.matchingIndicesListType = 'invalidIndexPattern';
}
if (hasNoInput()) {
return this.matchingIndicesListType = 'noInput';
}
if (this.hasExactMatches()) {
return this.matchingIndicesListType = 'exactMatches';
}
if (hasPartialMatches()) {
return this.matchingIndicesListType = 'partialMatches';
}
this.matchingIndicesListType = 'noMatches';
};
},
};
});

View file

@ -1,3 +0,0 @@
.createIndexPatternInputField.ng-untouched {
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.08), inset -400px 0 0 0 #fbfbfb;
}

View file

@ -73,6 +73,7 @@
</select>
<svg
ng-show="stepTimeField.canShowLoadingSelect()"
class="euiIcon euiFormControlLayout__icon euiFormControlLayout__icon--right euiIcon--medium"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"