mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
a169ac1093
commit
bc3f36095f
46 changed files with 2144 additions and 410 deletions
|
@ -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>
|
||||
`;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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'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>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export { Header } from './header';
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export { IndicesList } from './indices_list';
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export { LoadingIndices } from './loading_indices';
|
|
@ -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>
|
||||
);
|
|
@ -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>
|
||||
`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export { StatusMessage } from './status_message';
|
|
@ -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>
|
||||
|
||||
<strong>Success!</strong>
|
||||
|
||||
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't match any indices, but you have
|
||||
<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've entered doesn'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>
|
||||
);
|
||||
};
|
|
@ -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));
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 = ['\\', '/', '?', '"', '<', '>', '|', ' '];
|
|
@ -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
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"statusCode": 400,
|
||||
"error": "Bad Request"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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' }]
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export function createReasonableWait(cb) {
|
||||
return setTimeout(cb, 500);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -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));
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
import './step_index_pattern';
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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>
|
|
@ -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';
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue