[Rollups] Rollup support in Kibana, phase 1 (#21117) (#24554)

Enabled:
- View/Manage/Create rollup jobs

Disabled:
- Create a rollup index pattern
- Create rollup visualizations
- Add rollup visualizations to dashboards
- View raw rollup documents in Discover
This commit is contained in:
CJ Cenizal 2018-10-24 20:29:30 -07:00 committed by GitHub
parent 5041605782
commit 30d69b365b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
258 changed files with 12108 additions and 364 deletions

View file

@ -21,6 +21,12 @@
},
"title": {
"type": "text"
},
"type": {
"type": "keyword"
},
"typeMeta": {
"type": "keyword"
}
}
},

View file

@ -278,11 +278,17 @@ function discoverController(
.setField('highlightAll', true)
.setField('version', true);
// Even when searching rollups, we want to use the default strategy so that we get back a
// document-like response.
$scope.searchSource.setPreferredSearchStrategyId('default');
// searchSource which applies time range
const timeRangeSearchSource = savedSearch.searchSource.create();
timeRangeSearchSource.setField('filter', () => {
return timefilter.createFilter($scope.indexPattern);
});
if(isDefaultTypeIndexPattern($scope.indexPattern)) {
timeRangeSearchSource.setField('filter', () => {
return timefilter.createFilter($scope.indexPattern);
});
}
$scope.searchSource.setParent(timeRangeSearchSource);
@ -393,7 +399,7 @@ function discoverController(
$scope.opts = {
// number of records to fetch, then paginate through
sampleSize: config.get('discover:sampleSize'),
timefield: $scope.indexPattern.timeFieldName,
timefield: isDefaultTypeIndexPattern($scope.indexPattern) && $scope.indexPattern.timeFieldName,
savedSearch: savedSearch,
indexPatternList: $route.current.locals.ip.list,
};

View file

@ -37,6 +37,7 @@ import 'uiExports/fieldFormatEditors';
import 'uiExports/navbarExtensions';
import 'uiExports/contextMenuActions';
import 'uiExports/managementSections';
import 'uiExports/indexManagement';
import 'uiExports/devTools';
import 'uiExports/docViews';
import 'uiExports/embeddableFactories';

View file

@ -9,6 +9,7 @@
@import 'hacks';
// Core
// Core
@import 'management_app';
@import 'sections/settings/advanced_settings';
@import 'sections/settings/advanced_settings';
@import 'sections/indices/index';

View file

@ -3,8 +3,10 @@
exports[`CreateIndexPatternWizard defaults to the loading state 1`] = `
<div>
<Header
indexPatternName="name"
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<LoadingState />
</div>
@ -13,8 +15,10 @@ exports[`CreateIndexPatternWizard defaults to the loading state 1`] = `
exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = `
<div>
<Header
indexPatternName="name"
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<StepIndexPattern
allIndices={
@ -26,6 +30,16 @@ exports[`CreateIndexPatternWizard renders index pattern step when there are indi
}
esService={Object {}}
goToNextStep={[Function]}
indexPatternCreationType={
Object {
"checkIndicesForErrors": [Function],
"getIndexPatternMappings": [Function],
"getIndexPatternName": [Function],
"getIndexPatternType": [Function],
"getShowSystemIndices": [Function],
"renderPrompt": [Function],
}
}
initialQuery=""
isIncludingSystemIndices={false}
savedObjectsClient={Object {}}
@ -36,8 +50,10 @@ exports[`CreateIndexPatternWizard renders index pattern step when there are indi
exports[`CreateIndexPatternWizard renders the empty state when there are no indices 1`] = `
<div>
<Header
indexPatternName="name"
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<EmptyState
onRefresh={[Function]}
@ -48,13 +64,25 @@ exports[`CreateIndexPatternWizard renders the empty state when there are no indi
exports[`CreateIndexPatternWizard renders time field step when step is set to 2 1`] = `
<div>
<Header
indexPatternName="name"
isIncludingSystemIndices={false}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<StepTimeField
createIndexPattern={[Function]}
goToPreviousStep={[Function]}
indexPattern=""
indexPatternCreationType={
Object {
"checkIndicesForErrors": [Function],
"getIndexPatternMappings": [Function],
"getIndexPatternName": [Function],
"getIndexPatternType": [Function],
"getShowSystemIndices": [Function],
"renderPrompt": [Function],
}
}
indexPatternsService={Object {}}
/>
</div>
@ -63,8 +91,10 @@ exports[`CreateIndexPatternWizard renders time field step when step is set to 2
exports[`CreateIndexPatternWizard shows system indices even if there are no other indices if the include system indices is toggled 1`] = `
<div>
<Header
indexPatternName="name"
isIncludingSystemIndices={true}
onChangeIncludingSystemIndices={[Function]}
showSystemIndices={false}
/>
<StepIndexPattern
allIndices={
@ -76,6 +106,16 @@ exports[`CreateIndexPatternWizard shows system indices even if there are no othe
}
esService={Object {}}
goToNextStep={[Function]}
indexPatternCreationType={
Object {
"checkIndicesForErrors": [Function],
"getIndexPatternMappings": [Function],
"getIndexPatternName": [Function],
"getIndexPatternType": [Function],
"getShowSystemIndices": [Function],
"renderPrompt": [Function],
}
}
initialQuery=""
isIncludingSystemIndices={true}
savedObjectsClient={Object {}}

View file

@ -21,7 +21,14 @@ import React from 'react';
import { shallow } from 'enzyme';
import { CreateIndexPatternWizard } from '../create_index_pattern_wizard';
const mockIndexPatternCreationType = {
getIndexPatternType: () => 'default',
getIndexPatternName: () => 'name',
checkIndicesForErrors: () => false,
getShowSystemIndices: () => false,
renderPrompt: () => {},
getIndexPatternMappings: () => { return {}; }
};
jest.mock('../components/step_index_pattern', () => ({ StepIndexPattern: 'StepIndexPattern' }));
jest.mock('../components/step_time_field', () => ({ StepTimeField: 'StepTimeField' }));
jest.mock('../components/header', () => ({ Header: 'Header' }));
@ -44,6 +51,7 @@ const services = {
config: {},
changeUrl: () => {},
scopeApply: () => {},
indexPatternCreationType: mockIndexPatternCreationType,
};
describe('CreateIndexPatternWizard', () => {
@ -154,6 +162,7 @@ describe('CreateIndexPatternWizard', () => {
cache: { clear }
},
changeUrl,
indexPatternCreationType: mockIndexPatternCreationType
}}
/>
);

View file

@ -52,6 +52,7 @@ describe('CreateIndexPatternWizardRender', () => {
savedObjectsClient: {},
config: {},
changeUrl: () => {},
indexPatternCreationType: {},
}
);

View file

@ -10,9 +10,13 @@ exports[`Header should render normally 1`] = `
>
<h1>
<FormattedMessage
defaultMessage="Create index pattern"
defaultMessage="Create {indexPatternName}"
id="kbn.management.createIndexPatternHeader"
values={Object {}}
values={
Object {
"indexPatternName": undefined,
}
}
/>
</h1>
</EuiTitle>
@ -46,23 +50,6 @@ exports[`Header should render normally 1`] = `
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiSwitch
checked={true}
id="checkboxShowSystemIndices"
label={
<FormattedMessage
defaultMessage="Include system indices"
id="kbn.management.createIndexPattern.includeSystemIndicesToggleSwitchLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="m"
@ -80,9 +67,13 @@ exports[`Header should render without including system indices 1`] = `
>
<h1>
<FormattedMessage
defaultMessage="Create index pattern"
defaultMessage="Create {indexPatternName}"
id="kbn.management.createIndexPatternHeader"
values={Object {}}
values={
Object {
"indexPatternName": undefined,
}
}
/>
</h1>
</EuiTitle>
@ -116,23 +107,6 @@ exports[`Header should render without including system indices 1`] = `
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiSwitch
checked={false}
id="checkboxShowSystemIndices"
label={
<FormattedMessage
defaultMessage="Include system indices"
id="kbn.management.createIndexPattern.includeSystemIndicesToggleSwitchLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer
size="m"

View file

@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import React, { Fragment } from 'react';
import {
EuiSpacer,
@ -32,6 +32,9 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
export const Header = ({
prompt,
indexPatternName,
showSystemIndices,
isIncludingSystemIndices,
onChangeIncludingSystemIndices,
}) => (
@ -41,7 +44,10 @@ export const Header = ({
<h1>
<FormattedMessage
id="kbn.management.createIndexPatternHeader"
defaultMessage="Create index pattern"
defaultMessage="Create {indexPatternName}"
values={{
indexPatternName
}}
/>
</h1>
</EuiTitle>
@ -58,18 +64,30 @@ export const Header = ({
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSwitch
label={<FormattedMessage
id="kbn.management.createIndexPattern.includeSystemIndicesToggleSwitchLabel"
defaultMessage="Include system indices"
/>}
id="checkboxShowSystemIndices"
checked={isIncludingSystemIndices}
onChange={onChangeIncludingSystemIndices}
/>
</EuiFlexItem>
{
showSystemIndices ? (
<EuiFlexItem grow={false}>
<EuiSwitch
label={<FormattedMessage
id="kbn.management.createIndexPattern.includeSystemIndicesToggleSwitchLabel"
defaultMessage="Include system indices"
/>}
id="checkboxShowSystemIndices"
checked={isIncludingSystemIndices}
onChange={onChangeIncludingSystemIndices}
/>
</EuiFlexItem>
) : null
}
</EuiFlexGroup>
{
prompt ? (
<Fragment>
<EuiSpacer size="s" />
{prompt}
</Fragment>
) : null
}
<EuiSpacer size="m"/>
</div>
);

View file

@ -7,7 +7,7 @@ Object {
data-test-subj="createIndexPatternStep1Header"
errors={
Array [
"An index pattern cannot contain spaces or the characters: {characterList}",
"A {indexPatternName} cannot contain spaces or the characters: {characterList}",
]
}
goToNextStep={[Function]}
@ -19,38 +19,42 @@ Object {
"i18n": Array [
Array [
Object {
"defaultMessage": "An index pattern cannot contain spaces or the characters: {characterList}",
"defaultMessage": "A {indexPatternName} cannot contain spaces or the characters: {characterList}",
"id": "kbn.management.createIndexPattern.step.invalidCharactersErrorMessage",
},
Object {
"characterList": "\\\\, /, ?, \\", <, >, |",
"indexPatternName": "name",
},
],
Array [
Object {
"defaultMessage": "An index pattern cannot contain spaces or the characters: {characterList}",
"defaultMessage": "A {indexPatternName} cannot contain spaces or the characters: {characterList}",
"id": "kbn.management.createIndexPattern.step.invalidCharactersErrorMessage",
},
Object {
"characterList": "\\\\, /, ?, \\", <, >, |",
"indexPatternName": "name",
},
],
Array [
Object {
"defaultMessage": "An index pattern cannot contain spaces or the characters: {characterList}",
"defaultMessage": "A {indexPatternName} cannot contain spaces or the characters: {characterList}",
"id": "kbn.management.createIndexPattern.step.invalidCharactersErrorMessage",
},
Object {
"characterList": "\\\\, /, ?, \\", <, >, |",
"indexPatternName": "name",
},
],
Array [
Object {
"defaultMessage": "An index pattern cannot contain spaces or the characters: {characterList}",
"defaultMessage": "A {indexPatternName} cannot contain spaces or the characters: {characterList}",
"id": "kbn.management.createIndexPattern.step.invalidCharactersErrorMessage",
},
Object {
"characterList": "\\\\, /, ?, \\", <, >, |",
"indexPatternName": "name",
},
],
],

View file

@ -25,7 +25,12 @@ import { Header } from '../components/header';
jest.mock('../../../lib/ensure_minimum_time', () => ({
ensureMinimumTime: async (promises) => Array.isArray(promises) ? await Promise.all(promises) : await promises
}));
const mockIndexPatternCreationType = {
getIndexPatternType: () => 'default',
getIndexPatternName: () => 'name',
checkIndicesForErrors: () => false,
getShowSystemIndices: () => false
};
// If we don't mock this, Jest fails with the error `TypeError: Cannot redefine property: prototype
// at Function.defineProperties`.
jest.mock('ui/index_patterns', () => ({
@ -39,7 +44,7 @@ jest.mock('ui/chrome', () => ({
}));
jest.mock('../../../lib/get_indices', () => ({
getIndices: (service, query) => {
getIndices: (service, indexPatternCreationType, query) => {
if (query.startsWith('e')) {
return [
{ name: 'es' },
@ -67,6 +72,7 @@ const createComponent = props => {
esService={esService}
savedObjectsClient={savedObjectsClient}
goToNextStep={goToNextStep}
indexPatternCreationType={mockIndexPatternCreationType}
{...props}
/>
);

View file

@ -15,6 +15,10 @@ exports[`IndicesList should change pages 1`] = `
>
kibana
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="1"
@ -25,6 +29,10 @@ exports[`IndicesList should change pages 1`] = `
>
es
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
</EuiTableBody>
</EuiTable>
@ -134,6 +142,10 @@ exports[`IndicesList should change per page 1`] = `
>
kibana
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
</EuiTableBody>
</EuiTable>
@ -258,6 +270,10 @@ exports[`IndicesList should highlight the query in the matches 1`] = `
bana
</span>
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="1"
@ -268,6 +284,10 @@ exports[`IndicesList should highlight the query in the matches 1`] = `
>
es
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
</EuiTableBody>
</EuiTable>
@ -377,6 +397,10 @@ exports[`IndicesList should render normally 1`] = `
>
kibana
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="1"
@ -387,6 +411,10 @@ exports[`IndicesList should render normally 1`] = `
>
es
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
</EuiTableBody>
</EuiTable>
@ -496,6 +524,10 @@ exports[`IndicesList updating props should render all new indices 1`] = `
>
kibana
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="1"
@ -506,6 +538,10 @@ exports[`IndicesList updating props should render all new indices 1`] = `
>
es
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="2"
@ -516,6 +552,10 @@ exports[`IndicesList updating props should render all new indices 1`] = `
>
kibana
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="3"
@ -526,6 +566,10 @@ exports[`IndicesList updating props should render all new indices 1`] = `
>
es
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="4"
@ -536,6 +580,10 @@ exports[`IndicesList updating props should render all new indices 1`] = `
>
kibana
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="5"
@ -546,6 +594,10 @@ exports[`IndicesList updating props should render all new indices 1`] = `
>
es
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="6"
@ -556,6 +608,10 @@ exports[`IndicesList updating props should render all new indices 1`] = `
>
kibana
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="7"
@ -566,6 +622,10 @@ exports[`IndicesList updating props should render all new indices 1`] = `
>
es
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="8"
@ -576,6 +636,10 @@ exports[`IndicesList updating props should render all new indices 1`] = `
>
kibana
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
<EuiTableRow
key="9"
@ -586,6 +650,10 @@ exports[`IndicesList updating props should render all new indices 1`] = `
>
es
</EuiTableRowCell>
<EuiTableRowCell
align="left"
textOnly={true}
/>
</EuiTableRow>
</EuiTableBody>
</EuiTable>

View file

@ -22,8 +22,8 @@ import { IndicesList } from '../indices_list';
import { shallow } from 'enzyme';
const indices = [
{ name: 'kibana' },
{ name: 'es' }
{ name: 'kibana', tags: [] },
{ name: 'es', tags: [] }
];
describe('IndicesList', () => {

View file

@ -22,6 +22,7 @@ import PropTypes from 'prop-types';
import { PER_PAGE_INCREMENTS } from '../../../../constants';
import {
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
@ -185,6 +186,13 @@ export class IndicesList extends Component {
<EuiTableRowCell>
{this.highlightIndexName(index.name, queryWithoutWildcard)}
</EuiTableRowCell>
<EuiTableRowCell>
{index.tags.map(tag => {
return (
<EuiBadge key={`index_${key}_tag_${tag.key}`} color="primary">{tag.name}</EuiBadge>
);
})}
</EuiTableRowCell>
</EuiTableRow>
);
});

View file

@ -174,8 +174,8 @@ exports[`StatusMessage should show that system indices exist 1`] = `
>
<span>
<FormattedMessage
defaultMessage="No Elasticsearch indices match your pattern. To view the matching system indices, toggle the switch in the upper right."
id="kbn.management.createIndexPattern.step.status.noSystemIndicesWithPromptLabel"
defaultMessage="No Elasticsearch indices match your pattern."
id="kbn.management.createIndexPattern.step.status.noSystemIndicesLabel"
values={Object {}}
/>
</span>

View file

@ -35,6 +35,7 @@ export const StatusMessage = ({
},
isIncludingSystemIndices,
query,
showSystemIndicies,
}) => {
let statusIcon;
let statusMessage;
@ -58,7 +59,7 @@ export const StatusMessage = ({
</span>
);
}
else if (!isIncludingSystemIndices) {
else if (!isIncludingSystemIndices && showSystemIndicies) {
statusMessage = (
<span>
<FormattedMessage
@ -70,7 +71,6 @@ export const StatusMessage = ({
);
}
else {
// This should never really happen but let's handle it just in case
statusMessage = (
<span>
<FormattedMessage

View file

@ -50,6 +50,7 @@ export class StepIndexPatternComponent extends Component {
isIncludingSystemIndices: PropTypes.bool.isRequired,
esService: PropTypes.object.isRequired,
savedObjectsClient: PropTypes.object.isRequired,
indexPatternCreationType: PropTypes.object.isRequired,
goToNextStep: PropTypes.func.isRequired,
initialQuery: PropTypes.string,
}
@ -60,6 +61,7 @@ export class StepIndexPatternComponent extends Component {
constructor(props) {
super(props);
const { indexPatternCreationType } = this.props;
this.state = {
partialMatchedIndices: [],
exactMatchedIndices: [],
@ -69,8 +71,10 @@ export class StepIndexPatternComponent extends Component {
query: props.initialQuery,
appendedWildcard: false,
showingIndexPatternQueryErrors: false,
indexPatternName: indexPatternCreationType.getIndexPatternName(),
};
this.ILLEGAL_CHARACTERS = [...ILLEGAL_CHARACTERS];
this.lastQuery = null;
}
@ -93,7 +97,7 @@ export class StepIndexPatternComponent extends Component {
}
fetchIndices = async (query) => {
const { esService } = this.props;
const { esService, indexPatternCreationType } = this.props;
const { existingIndexPatterns } = this.state;
if (existingIndexPatterns.includes(query)) {
@ -104,7 +108,7 @@ export class StepIndexPatternComponent extends Component {
this.setState({ isLoadingIndices: true, indexPatternExists: false });
if (query.endsWith('*')) {
const exactMatchedIndices = await ensureMinimumTime(getIndices(esService, query, MAX_SEARCH_SIZE));
const exactMatchedIndices = await ensureMinimumTime(getIndices(esService, indexPatternCreationType, query, MAX_SEARCH_SIZE));
// If the search changed, discard this state
if (query !== this.lastQuery) {
return;
@ -117,8 +121,8 @@ export class StepIndexPatternComponent extends Component {
partialMatchedIndices,
exactMatchedIndices,
] = await ensureMinimumTime([
getIndices(esService, `${query}*`, MAX_SEARCH_SIZE),
getIndices(esService, query, MAX_SEARCH_SIZE),
getIndices(esService, indexPatternCreationType, `${query}*`, MAX_SEARCH_SIZE),
getIndices(esService, indexPatternCreationType, query, MAX_SEARCH_SIZE),
]);
// If the search changed, discard this state
@ -167,6 +171,7 @@ export class StepIndexPatternComponent extends Component {
}
renderStatusMessage(matchedIndices) {
const { indexPatternCreationType } = this.props;
const { query, isLoadingIndices, indexPatternExists, isIncludingSystemIndices } = this.state;
if (isLoadingIndices || indexPatternExists) {
@ -176,6 +181,7 @@ export class StepIndexPatternComponent extends Component {
return (
<StatusMessage
matchedIndices={matchedIndices}
showSystemIndices={indexPatternCreationType.getShowSystemIndices()}
isIncludingSystemIndices={isIncludingSystemIndices}
query={query}
/>
@ -230,27 +236,31 @@ export class StepIndexPatternComponent extends Component {
}
renderHeader({ exactMatchedIndices: indices }) {
const { goToNextStep, intl } = this.props;
const { query, showingIndexPatternQueryErrors, indexPatternExists } = this.state;
const { goToNextStep, indexPatternCreationType, intl } = this.props;
const { query, showingIndexPatternQueryErrors, indexPatternExists, indexPatternName } = this.state;
let containsErrors = false;
const errors = [];
const characterList = ILLEGAL_CHARACTERS.slice(0, ILLEGAL_CHARACTERS.length - 1).join(', ');
const characterList = this.ILLEGAL_CHARACTERS.slice(0, this.ILLEGAL_CHARACTERS.length - 1).join(', ');
const checkIndices = indexPatternCreationType.checkIndicesForErrors(indices);
if (!query || !query.length || query === '.' || query === '..') {
// This is an error scenario but do not report an error
containsErrors = true;
}
else if (containsIllegalCharacters(query, ILLEGAL_CHARACTERS)) {
} else if (containsIllegalCharacters(query, ILLEGAL_CHARACTERS)) {
const errorMessage = intl.formatMessage(
{
id: 'kbn.management.createIndexPattern.step.invalidCharactersErrorMessage',
defaultMessage: 'An index pattern cannot contain spaces or the characters: {characterList}'
defaultMessage: 'A {indexPatternName} cannot contain spaces or the characters: {characterList}'
},
{ characterList });
{ characterList, indexPatternName }
);
errors.push(errorMessage);
containsErrors = true;
} else if (checkIndices) {
errors.push(...checkIndices);
containsErrors = true;
}
const isInputInvalid = showingIndexPatternQueryErrors && containsErrors && errors.length > 0;

View file

@ -63,9 +63,10 @@ exports[`StepTimeField should render a selected timeField 1`] = `
>
<Header
indexPattern="ki*"
indexPatternName="name"
/>
<EuiSpacer
size="xs"
size="m"
/>
<TimeField
fetchTimeFields={[Function]}
@ -120,9 +121,10 @@ exports[`StepTimeField should render advanced options 1`] = `
>
<Header
indexPattern="ki*"
indexPatternName="name"
/>
<EuiSpacer
size="xs"
size="m"
/>
<TimeField
fetchTimeFields={[Function]}
@ -166,9 +168,10 @@ exports[`StepTimeField should render advanced options with an index pattern id 1
>
<Header
indexPattern="ki*"
indexPatternName="name"
/>
<EuiSpacer
size="xs"
size="m"
/>
<TimeField
fetchTimeFields={[Function]}
@ -212,9 +215,10 @@ exports[`StepTimeField should render normally 1`] = `
>
<Header
indexPattern="ki*"
indexPatternName="name"
/>
<EuiSpacer
size="xs"
size="m"
/>
<TimeField
fetchTimeFields={[Function]}
@ -258,9 +262,10 @@ exports[`StepTimeField should render timeFields 1`] = `
>
<Header
indexPattern="ki*"
indexPatternName="name"
/>
<EuiSpacer
size="xs"
size="m"
/>
<TimeField
fetchTimeFields={[Function]}

View file

@ -30,6 +30,10 @@ jest.mock('../../../lib/extract_time_fields', () => ({
extractTimeFields: fields => fields,
}));
const mockIndexPatternCreationType = {
getIndexPatternType: () => 'default',
getIndexPatternName: () => 'name'
};
const noop = () => {};
const indexPatternsService = {
fieldsFetcher: {
@ -45,6 +49,7 @@ describe('StepTimeField', () => {
indexPatternsService={indexPatternsService}
goToPreviousStep={noop}
createIndexPattern={noop}
indexPatternCreationType={mockIndexPatternCreationType}
/>
);
@ -58,6 +63,7 @@ describe('StepTimeField', () => {
indexPatternsService={indexPatternsService}
goToPreviousStep={noop}
createIndexPattern={noop}
indexPatternCreationType={mockIndexPatternCreationType}
/>
);
@ -78,6 +84,7 @@ describe('StepTimeField', () => {
indexPatternsService={indexPatternsService}
goToPreviousStep={noop}
createIndexPattern={noop}
indexPatternCreationType={mockIndexPatternCreationType}
/>
);
@ -100,6 +107,7 @@ describe('StepTimeField', () => {
indexPatternsService={indexPatternsService}
goToPreviousStep={noop}
createIndexPattern={noop}
indexPatternCreationType={mockIndexPatternCreationType}
/>
);
@ -128,6 +136,7 @@ describe('StepTimeField', () => {
indexPatternsService={indexPatternsService}
goToPreviousStep={noop}
createIndexPattern={noop}
indexPatternCreationType={mockIndexPatternCreationType}
/>
);
@ -151,6 +160,7 @@ describe('StepTimeField', () => {
indexPatternsService={indexPatternsService}
goToPreviousStep={noop}
createIndexPattern={noop}
indexPatternCreationType={mockIndexPatternCreationType}
/>
);
@ -174,6 +184,7 @@ describe('StepTimeField', () => {
indexPatternsService={indexPatternsService}
goToPreviousStep={noop}
createIndexPattern={noop}
indexPatternCreationType={mockIndexPatternCreationType}
/>
);
@ -189,6 +200,7 @@ describe('StepTimeField', () => {
indexPatternsService={indexPatternsService}
goToPreviousStep={noop}
createIndexPattern={noop}
indexPatternCreationType={mockIndexPatternCreationType}
/>
);
@ -207,6 +219,7 @@ describe('StepTimeField', () => {
indexPatternsService={indexPatternsService}
goToPreviousStep={noop}
createIndexPattern={noop}
indexPatternCreationType={mockIndexPatternCreationType}
/>
);

View file

@ -21,13 +21,14 @@ exports[`Header should render normally 1`] = `
grow={true}
>
<FormattedMessage
defaultMessage="You've defined {indexPattern} as your index pattern. Now you can specify some settings before we create it."
defaultMessage="You've defined {indexPattern} as your {indexPatternName}. Now you can specify some settings before we create it."
id="kbn.management.createIndexPattern.stepTimeLabel"
values={
Object {
"indexPattern": <strong>
ki*
</strong>,
"indexPatternName": undefined,
}
}
/>

View file

@ -29,6 +29,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
export const Header = ({
indexPattern,
indexPatternName,
}) => (
<div>
<EuiTitle size="s">
@ -43,8 +44,11 @@ export const Header = ({
<EuiText color="subdued">
<FormattedMessage
id="kbn.management.createIndexPattern.stepTimeLabel"
defaultMessage="You've defined {indexPattern} as your index pattern. Now you can specify some settings before we create it."
values={{ indexPattern: <strong>{indexPattern}</strong> }}
defaultMessage="You've defined {indexPattern} as your {indexPatternName}. Now you can specify some settings before we create it."
values={{
indexPattern: <strong>{indexPattern}</strong>,
indexPatternName,
}}
/>
</EuiText>
</div>

View file

@ -43,11 +43,17 @@ export class StepTimeFieldComponent extends Component {
indexPatternsService: PropTypes.object.isRequired,
goToPreviousStep: PropTypes.func.isRequired,
createIndexPattern: PropTypes.func.isRequired,
indexPatternCreationType: PropTypes.object.isRequired,
}
constructor(props) {
super(props);
const {
getIndexPatternType,
getIndexPatternName,
} = props.indexPatternCreationType;
this.state = {
timeFields: [],
selectedTimeField: undefined,
@ -56,6 +62,8 @@ export class StepTimeFieldComponent extends Component {
isFetchingTimeFields: false,
isCreating: false,
indexPatternId: '',
indexPatternType: getIndexPatternType(),
indexPatternName: getIndexPatternName(),
};
}
@ -65,9 +73,12 @@ export class StepTimeFieldComponent extends Component {
fetchTimeFields = async () => {
const { indexPatternsService, indexPattern } = this.props;
const { getFetchForWildcardOptions } = this.props.indexPatternCreationType;
this.setState({ isFetchingTimeFields: true });
const fields = await ensureMinimumTime(indexPatternsService.fieldsFetcher.fetchForWildcard(indexPattern));
const fields = await ensureMinimumTime(
indexPatternsService.fieldsFetcher.fetchForWildcard(indexPattern, getFetchForWildcardOptions())
);
const timeFields = extractTimeFields(fields);
this.setState({ timeFields, isFetchingTimeFields: false });
@ -113,6 +124,7 @@ export class StepTimeFieldComponent extends Component {
indexPatternId,
isCreating,
isFetchingTimeFields,
indexPatternName,
} = this.state;
if (isCreating) {
@ -156,8 +168,11 @@ export class StepTimeFieldComponent extends Component {
return (
<EuiPanel paddingSize="l">
<Header indexPattern={indexPattern} />
<EuiSpacer size="xs"/>
<Header
indexPattern={indexPattern}
indexPatternName={indexPatternName}
/>
<EuiSpacer size="m"/>
<TimeField
isVisible={showTimeField}
fetchTimeFields={this.fetchTimeFields}

View file

@ -20,7 +20,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ensureMinimumTime } from './lib';
import { StepIndexPattern } from './components/step_index_pattern';
import { StepTimeField } from './components/step_time_field';
import { Header } from './components/header';
@ -28,7 +27,10 @@ import { LoadingState } from './components/loading_state';
import { EmptyState } from './components/empty_state';
import { MAX_SEARCH_SIZE } from './constants';
import { getIndices } from './lib/get_indices';
import {
ensureMinimumTime,
getIndices,
} from './lib';
export class CreateIndexPatternWizard extends Component {
static propTypes = {
@ -37,6 +39,7 @@ export class CreateIndexPatternWizard extends Component {
es: PropTypes.object.isRequired,
indexPatterns: PropTypes.object.isRequired,
savedObjectsClient: PropTypes.object.isRequired,
indexPatternCreationType: PropTypes.object.isRequired,
config: PropTypes.object.isRequired,
changeUrl: PropTypes.func.isRequired,
}).isRequired,
@ -44,6 +47,7 @@ export class CreateIndexPatternWizard extends Component {
constructor(props) {
super(props);
this.indexPatternCreationType = this.props.services.indexPatternCreationType;
this.state = {
step: 1,
indexPattern: '',
@ -60,7 +64,7 @@ export class CreateIndexPatternWizard extends Component {
fetchIndices = async () => {
this.setState({ allIndices: [], isInitiallyLoadingIndices: true });
const { services } = this.props;
const allIndices = await ensureMinimumTime(getIndices(services.es, `*`, MAX_SEARCH_SIZE));
const allIndices = await ensureMinimumTime(getIndices(services.es, this.indexPatternCreationType, `*`, MAX_SEARCH_SIZE)); //
this.setState({ allIndices, isInitiallyLoadingIndices: false });
}
@ -74,6 +78,7 @@ export class CreateIndexPatternWizard extends Component {
id: indexPatternId,
title: indexPattern,
timeFieldName,
...this.indexPatternCreationType.getIndexPatternMappings()
});
const createdId = await emptyPattern.create();
@ -105,8 +110,11 @@ export class CreateIndexPatternWizard extends Component {
return (
<Header
prompt={this.indexPatternCreationType.renderPrompt()}
showSystemIndices={this.indexPatternCreationType.getShowSystemIndices()}
isIncludingSystemIndices={isIncludingSystemIndices}
onChangeIncludingSystemIndices={this.onChangeIncludingSystemIndices}
indexPatternName={this.indexPatternCreationType.getIndexPatternName()}
/>
);
}
@ -138,6 +146,7 @@ export class CreateIndexPatternWizard extends Component {
isIncludingSystemIndices={isIncludingSystemIndices}
esService={services.es}
savedObjectsClient={services.savedObjectsClient}
indexPatternCreationType={this.indexPatternCreationType}
goToNextStep={this.goToTimeFieldStep}
/>
);
@ -151,6 +160,7 @@ export class CreateIndexPatternWizard extends Component {
indexPatternsService={services.indexPatterns}
goToPreviousStep={this.goToIndexPatternStep}
createIndexPattern={this.createIndexPattern}
indexPatternCreationType={this.indexPatternCreationType}
/>
);
}

View file

@ -21,6 +21,7 @@ import { SavedObjectsClientProvider } from 'ui/saved_objects';
import uiRoutes from 'ui/routes';
import angularTemplate from './angular_template.html';
import 'ui/index_patterns';
import { IndexPatternCreationFactory } from 'ui/management/index_pattern_creation';
import { renderCreateIndexPatternWizard, destroyCreateIndexPatternWizard } from './render';
@ -29,13 +30,17 @@ uiRoutes.when('/management/kibana/index', {
controller: function ($scope, $injector) {
// Wait for the directives to execute
const kbnUrl = $injector.get('kbnUrl');
const Private = $injector.get('Private');
$scope.$$postDigest(() => {
const $routeParams = $injector.get('$routeParams');
const indexPatternCreationProvider = Private(IndexPatternCreationFactory)($routeParams.type);
const indexPatternCreationType = indexPatternCreationProvider.getType();
const services = {
config: $injector.get('config'),
es: $injector.get('es'),
indexPatterns: $injector.get('indexPatterns'),
savedObjectsClient: $injector.get('Private')(SavedObjectsClientProvider),
savedObjectsClient: Private(SavedObjectsClientProvider),
indexPatternCreationType,
changeUrl: url => {
$scope.$evalAsync(() => kbnUrl.changePath(url));
},

View file

@ -21,6 +21,15 @@ 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';
const mockIndexPatternCreationType = {
getIndexPatternType: () => 'default',
getIndexPatternName: () => 'name',
checkIndicesForErrors: () => false,
getShowSystemIndices: () => false,
renderPrompt: () => {},
getIndexPatternMappings: () => { return {}; },
getIndexTags: () => { return []; }
};
describe('getIndices', () => {
it('should work in a basic case', async () => {
@ -28,20 +37,20 @@ describe('getIndices', () => {
search: () => new Promise((resolve) => resolve(successfulResponse))
};
const result = await getIndices(es, 'kibana', 1);
const result = await getIndices(es, mockIndexPatternCreationType, '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);
expect((await getIndices(null, mockIndexPatternCreationType, '*:')).length).toBe(0);
});
it('should ignore a single comma', async () => {
expect((await getIndices(null, ',')).length).toBe(0);
expect((await getIndices(null, ',*')).length).toBe(0);
expect((await getIndices(null, ',foobar')).length).toBe(0);
expect((await getIndices(null, mockIndexPatternCreationType, ',')).length).toBe(0);
expect((await getIndices(null, mockIndexPatternCreationType, ',*')).length).toBe(0);
expect((await getIndices(null, mockIndexPatternCreationType, ',foobar')).length).toBe(0);
});
it('should trim the input', async () => {
@ -52,7 +61,7 @@ describe('getIndices', () => {
}),
};
await getIndices(es, 'kibana ', 1);
await getIndices(es, mockIndexPatternCreationType, 'kibana ', 1);
expect(index).toBe('kibana');
});
@ -64,7 +73,7 @@ describe('getIndices', () => {
}),
};
await getIndices(es, 'kibana', 10);
await getIndices(es, mockIndexPatternCreationType, 'kibana', 10);
expect(limit).toBe(10);
});
@ -74,7 +83,7 @@ describe('getIndices', () => {
search: () => new Promise((resolve) => resolve(errorResponse))
};
const result = await getIndices(es, 'kibana', 1);
const result = await getIndices(es, mockIndexPatternCreationType, 'kibana', 1);
expect(result.length).toBe(0);
});
@ -83,7 +92,7 @@ describe('getIndices', () => {
search: () => { throw new Error('Fail'); }
};
await expect(getIndices(es, 'kibana', 1)).rejects.toThrow();
await expect(getIndices(es, mockIndexPatternCreationType, 'kibana', 1)).rejects.toThrow();
});
it('should handle index_not_found_exception errors gracefully', async () => {
@ -91,12 +100,12 @@ describe('getIndices', () => {
search: () => new Promise((resolve, reject) => reject(exceptionResponse))
};
const result = await getIndices(es, 'kibana', 1);
const result = await getIndices(es, mockIndexPatternCreationType, 'kibana', 1);
expect(result.length).toBe(0);
});
it('should throw an exception if no limit is provided', async () => {
await expect(getIndices({}, 'kibana')).rejects.toThrow();
await expect(getIndices({}, mockIndexPatternCreationType, 'kibana')).rejects.toThrow();
});
});
});

View file

@ -19,7 +19,7 @@
import { get, sortBy } from 'lodash';
export async function getIndices(es, rawPattern, limit) {
export async function getIndices(es, indexPatternCreationType, rawPattern, limit) {
const pattern = rawPattern.trim();
// Searching for `*:` fails for CCS environments. The search request
@ -57,7 +57,7 @@ export async function getIndices(es, rawPattern, limit) {
size: limit,
}
}
}
},
}
};
@ -67,11 +67,18 @@ export async function getIndices(es, rawPattern, limit) {
return [];
}
return sortBy(response.aggregations.indices.buckets.map(bucket => {
return {
name: bucket.key
};
}), 'name');
return sortBy(
response.aggregations.indices.buckets.map(bucket => {
return bucket.key;
})
.map((indexName) => {
return {
name: indexName,
tags: indexPatternCreationType.getIndexTags(indexName)
};
})
, 'name'
);
}
catch (err) {
const type = get(err, 'body.error.caused_by.type');

View file

@ -15,15 +15,22 @@
delete="removePattern()"
></kbn-management-index-header>
<p ng-if="indexPattern.timeFieldName" class="kuiText kuiVerticalRhythm">
<span class="label label-success">
<span class="kuiIcon fa-clock-o"></span>
<span i18n-id="kbn.management.editIndexPattern.timeFilterHeader"
i18n-default-message="Time Filter field name: {timeFieldName}"
i18n-values="{ timeFieldName: indexPattern.timeFieldName }"></span>
<p class="kuiText kuiVerticalRhythm" ng-if="::(indexPattern.timeFieldName || (indexPattern.tags && indexPattern.tags.length))">
<span ng-if="::indexPattern.timeFieldName">
<span class="label label-success">
<span class="kuiIcon fa-clock-o"></span>
<span i18n-id="kbn.management.editIndexPattern.timeFilterHeader"
i18n-default-message="Time Filter field name: {timeFieldName}"
i18n-values="{ timeFieldName: indexPattern.timeFieldName }"></span>
</span>
&nbsp;
</span>
<span ng-repeat="tag in indexPattern.tags">
<span class="label label-info">{{tag.name}}</span>
</span>
</p>
<p class="kuiText kuiVerticalRhythm">
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
i18n-default-message="This page lists every field in the {indexPatternTitle} index and the field's associated core type as recorded by Elasticsearch. To change a field type, use the Elasticsearch"

View file

@ -27,6 +27,7 @@ import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import template from './edit_index_pattern.html';
import { FieldWildcardProvider } from 'ui/field_wildcard';
import { IndexPatternListFactory } from 'ui/management/index_pattern_list';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { SourceFiltersTable } from './source_filters_table';
@ -135,6 +136,7 @@ function updateIndexedFieldsTable($scope, $state) {
$scope.kbnUrl.redirectToRoute(obj, route);
$scope.$apply();
},
getFieldInfo: $scope.getFieldInfo,
}}
/>
</I18nProvider>,
@ -184,11 +186,14 @@ uiModules.get('apps/management')
$scope, $location, $route, config, indexPatterns, Private, AppState, docTitle, confirmModal) {
const $state = $scope.state = new AppState();
const { fieldWildcardMatcher } = Private(FieldWildcardProvider);
const indexPatternListProvider = Private(IndexPatternListFactory)();
$scope.fieldWildcardMatcher = fieldWildcardMatcher;
$scope.editSectionsProvider = Private(IndicesEditSectionsProvider);
$scope.kbnUrl = Private(KbnUrlProvider);
$scope.indexPattern = $route.current.locals.indexPattern;
$scope.indexPattern.tags = indexPatternListProvider.getIndexPatternTags($scope.indexPattern);
$scope.getFieldInfo = indexPatternListProvider.getFieldInfo;
docTitle.change($scope.indexPattern.title);
const otherPatterns = _.filter($route.current.locals.indexPatterns, pattern => {
@ -196,7 +201,7 @@ uiModules.get('apps/management')
});
$scope.$watch('indexPattern.fields', function () {
$scope.editSections = $scope.editSectionsProvider($scope.indexPattern);
$scope.editSections = $scope.editSectionsProvider($scope.indexPattern, indexPatternListProvider);
$scope.refreshFilters();
$scope.fields = $scope.indexPattern.getNonScriptedFields();
updateIndexedFieldsTable($scope, $state);

View file

@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
export function IndicesEditSectionsProvider() {
return function (indexPattern) {
return function (indexPattern, indexPatternListProvider) {
const fieldCount = _.countBy(indexPattern.fields, function (field) {
return (field.scripted) ? 'scripted' : 'indexed';
});
@ -33,22 +33,28 @@ export function IndicesEditSectionsProvider() {
sourceFilters: indexPattern.sourceFilters ? indexPattern.sourceFilters.length : 0,
});
return [
{
title: i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', { defaultMessage: 'Fields' }),
index: 'indexedFields',
count: fieldCount.indexed
},
{
const editSections = [];
editSections.push({
title: i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', { defaultMessage: 'Fields' }),
index: 'indexedFields',
count: fieldCount.indexed
});
if(indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) {
editSections.push({
title: i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', { defaultMessage: 'Scripted fields' }),
index: 'scriptedFields',
count: fieldCount.scripted
},
{
title: i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', { defaultMessage: 'Source filters' }),
index: 'sourceFilters',
count: fieldCount.sourceFilters
}
];
});
}
editSections.push({
title: i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', { defaultMessage: 'Source filters' }),
index: 'sourceFilters',
count: fieldCount.sourceFilters
});
return editSections;
};
}

View file

@ -17,6 +17,7 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = `
"excluded": false,
"format": undefined,
"indexPattern": undefined,
"info": undefined,
"name": "Elastic",
"routes": undefined,
"searchable": true,
@ -45,6 +46,7 @@ exports[`IndexedFieldsTable should filter based on the type filter 1`] = `
"excluded": false,
"format": undefined,
"indexPattern": undefined,
"info": undefined,
"name": "timestamp",
"routes": undefined,
"type": "date",
@ -73,6 +75,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
"excluded": false,
"format": undefined,
"indexPattern": undefined,
"info": undefined,
"name": "Elastic",
"routes": undefined,
"searchable": true,
@ -82,6 +85,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
"excluded": false,
"format": undefined,
"indexPattern": undefined,
"info": undefined,
"name": "timestamp",
"routes": undefined,
"type": "date",
@ -91,6 +95,7 @@ exports[`IndexedFieldsTable should render normally 1`] = `
"excluded": false,
"format": undefined,
"indexPattern": undefined,
"info": undefined,
"name": "conflictingField",
"routes": undefined,
"type": "conflict",

View file

@ -97,16 +97,19 @@ exports[`Table should render normally 1`] = `
Array [
Object {
"displayName": "Elastic",
"info": Object {},
"name": "Elastic",
"searchable": true,
},
Object {
"displayName": "timestamp",
"info": Object {},
"name": "timestamp",
"type": "date",
},
Object {
"displayName": "conflictingField",
"info": Object {},
"name": "conflictingField",
"type": "conflict",
},

View file

@ -28,9 +28,9 @@ const indexPattern = {
};
const items = [
{ name: 'Elastic', displayName: 'Elastic', searchable: true },
{ name: 'timestamp', displayName: 'timestamp', type: 'date' },
{ name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' },
{ name: 'Elastic', displayName: 'Elastic', searchable: true, info: {} },
{ name: 'timestamp', displayName: 'timestamp', type: 'date', info: {} },
{ name: 'conflictingField', displayName: 'conflictingField', type: 'conflict', info: {} },
];
describe('Table', () => {
@ -55,7 +55,7 @@ describe('Table', () => {
/>
);
const tableCell = shallow(component.prop('columns')[0].render('Elastic'));
const tableCell = shallow(component.prop('columns')[0].render('Elastic', items[0]));
expect(tableCell).toMatchSnapshot();
});
@ -68,7 +68,7 @@ describe('Table', () => {
/>
);
const tableCell = shallow(component.prop('columns')[0].render('timestamp', true));
const tableCell = shallow(component.prop('columns')[0].render('timestamp', items[1]));
expect(tableCell).toMatchSnapshot();
});
@ -94,7 +94,7 @@ describe('Table', () => {
/>
);
const tableCell = shallow(component.prop('columns')[3].render(false));
const tableCell = shallow(component.prop('columns')[3].render(false, items[2]));
expect(tableCell).toMatchSnapshot();
});

View file

@ -39,13 +39,19 @@ export class TableComponent extends PureComponent {
return value ? <EuiIcon type="dot" color="secondary" aria-label={label}/> : <span/>;
}
renderFieldName(name, isTimeField) {
renderFieldName(name, field) {
const { indexPattern } = this.props;
const { intl } = this.props;
const label = intl.formatMessage({
id: 'kbn.management.editIndexPattern.fields.table.primaryTimeAria',
const infoLabel = intl.formatMessage({
id: 'kbn.management.editIndexPattern.fields.table.additionalInfoAriaLabel',
defaultMessage: 'Additional field information'
});
const timeLabel = intl.formatMessage({
id: 'kbn.management.editIndexPattern.fields.table.primaryTimeAriaLabel',
defaultMessage: 'Primary time field'
});
const content = intl.formatMessage({
const timeContent = intl.formatMessage({
id: 'kbn.management.editIndexPattern.fields.table.primaryTimeTooltip',
defaultMessage: 'This field represents the time that events occurred.'
});
@ -53,17 +59,28 @@ export class TableComponent extends PureComponent {
return (
<span>
{name}
{isTimeField ? (
{field.info && field.info.length ? (
<span>
&nbsp;
<EuiIconTip
type="questionInCircle"
color="primary"
aria-label={infoLabel}
content={field.info.map((info, i) => <div key={i}>{info}</div>)}
/>
</span>
) : null}
{indexPattern.timeFieldName === name ? (
<span>
&nbsp;
<EuiIconTip
type="clock"
color="primary"
aria-label={label}
content={content}
aria-label={timeLabel}
content={timeContent}
/>
</span>
) : ''}
) : null}
</span>
);
}
@ -98,7 +115,7 @@ export class TableComponent extends PureComponent {
}
render() {
const { indexPattern, items, editField, intl } = this.props;
const { items, editField, intl } = this.props;
const pagination = {
initialPageSize: 10,
@ -111,8 +128,8 @@ export class TableComponent extends PureComponent {
name: intl.formatMessage({ id: 'kbn.management.editIndexPattern.fields.table.nameHeader', defaultMessage: 'Name' }),
dataType: 'string',
sortable: true,
render: (value) => {
return this.renderFieldName(value, indexPattern.timeFieldName === value);
render: (value, field) => {
return this.renderFieldName(value, field);
},
width: '38%',
'data-test-subj': 'indexedFieldName',

View file

@ -36,6 +36,7 @@ export class IndexedFieldsTable extends Component {
indexedFieldTypeFilter: PropTypes.string,
helpers: PropTypes.shape({
redirectToRoute: PropTypes.func.isRequired,
getFieldInfo: PropTypes.func,
}),
fieldWildcardMatcher: PropTypes.func.isRequired,
}
@ -57,7 +58,7 @@ export class IndexedFieldsTable extends Component {
}
mapFields(fields) {
const { indexPattern, fieldWildcardMatcher } = this.props;
const { indexPattern, fieldWildcardMatcher, helpers } = this.props;
const sourceFilters = indexPattern.sourceFilters && indexPattern.sourceFilters.map(f => f.value);
const fieldWildcardMatch = fieldWildcardMatcher(sourceFilters || []);
@ -70,6 +71,7 @@ export class IndexedFieldsTable extends Component {
indexPattern: field.indexPattern,
format: getFieldFormat(indexPattern, field.name),
excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false,
info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field.name),
};
}) || [];
}

View file

@ -1,52 +1,7 @@
<div class="euiPage">
<div class="col-md-2 sidebar-container" role="region" aria-label="{{::'kbn.management.editIndexPatternAria' | i18n: { defaultMessage: 'Index patterns' } }}">
<div class="sidebar-list">
<div class="sidebar-item-title full-title">
<h5 data-test-subj="createIndexPatternParent">
<a
ng-if="editingId"
href="#/management/kibana/index"
class="kuiButton kuiButton--primary kuiButton--small"
data-test-subj="createIndexPatternButton"
>
<span aria-hidden="true" class="kuiIcon fa-plus"></span>
<span i18n-id="kbn.management.editIndexPattern.createIndexButton"
i18n-default-message="Create Index Pattern"></span>
</a>
</h5>
</div>
<ul class="list-unstyled">
<li
ng-if="!defaultIndex"
class="sidebar-item"
>
<div class="sidebar-item-title full-title">
<div class="euiText euiText--extraSmall">
<div class="euiBadge euiBadge--warning"
i18n-id="kbn.management.editIndexPattern.createIndex.warningHeader"
i18n-default-message="Warning"></div>
<p i18n-id="kbn.management.editIndexPattern.createIndex.warningLabel"
i18n-default-message="No default index pattern. You must select or create one to continue."></p>
</div>
</div>
</li>
<li
ng-repeat="pattern in indexPatternList | orderBy:['-default','title'] track by pattern.id"
class="sidebar-item"
>
<a
href="{{::pattern.url}}"
class="euiLink euiLink--primary show"
data-test-subj="indexPatternLink"
>
<div class="{{::pattern.class}}">
<i aria-hidden="true" ng-if="pattern.default" class="fa fa-star"></i>
<span ng-bind="::pattern.title"></span>
</div>
</a>
</li>
</ul>
<div id="indexPatternListReact" role="region" aria-label="{{'kbn.management.editIndexPatternLiveRegionAriaLabel' | i18n: { defaultMessage: 'Index patterns' } }}"></div>
</div>
</div>

View file

@ -18,6 +18,8 @@
*/
import { management } from 'ui/management';
import { IndexPatternListFactory } from 'ui/management/index_pattern_list';
import { IndexPatternCreationFactory } from 'ui/management/index_pattern_creation';
import './create_index_pattern_wizard';
import './edit_index_pattern';
import uiRoutes from 'ui/routes';
@ -27,13 +29,45 @@ import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { IndexPatternList } from './index_pattern_list';
const INDEX_PATTERN_LIST_DOM_ELEMENT_ID = 'indexPatternListReact';
export function updateIndexPatternList(
$scope,
indexPatternCreationOptions,
defaultIndex,
indexPatterns,
) {
const node = document.getElementById(INDEX_PATTERN_LIST_DOM_ELEMENT_ID);
if (!node) {
return;
}
render(
<IndexPatternList
indexPatternCreationOptions={indexPatternCreationOptions}
defaultIndex={defaultIndex}
indexPatterns={indexPatterns}
/>,
node,
);
}
export const destroyIndexPatternList = () => {
const node = document.getElementById(INDEX_PATTERN_LIST_DOM_ELEMENT_ID);
node && unmountComponentAtNode(node);
};
const indexPatternsResolutions = {
indexPatterns: function (Private) {
const savedObjectsClient = Private(SavedObjectsClientProvider);
return savedObjectsClient.find({
type: 'index-pattern',
fields: ['title'],
fields: ['title', 'type'],
perPage: 10000
}).then(response => response.savedObjects);
}
@ -52,28 +86,55 @@ uiRoutes
// wrapper directive, which sets some global stuff up like the left nav
uiModules.get('apps/management')
.directive('kbnManagementIndices', function ($route, config, kbnUrl) {
.directive('kbnManagementIndices', function ($route, config, kbnUrl, Private) {
return {
restrict: 'E',
transclude: true,
template: indexTemplate,
link: function ($scope) {
$scope.editingId = $route.current.params.indexPatternId;
config.bindToScope($scope, 'defaultIndex');
link: async function ($scope) {
const indexPatternListProvider = Private(IndexPatternListFactory)();
const indexPatternCreationProvider = Private(IndexPatternCreationFactory)();
const indexPatternCreationOptions = await indexPatternCreationProvider.getIndexPatternCreationOptions((url) => {
$scope.$evalAsync(() => kbnUrl.change(url));
});
$scope.$watch('defaultIndex', function () {
const renderList = () => {
$scope.indexPatternList = $route.current.locals.indexPatterns.map(pattern => {
const id = pattern.id;
const tags = indexPatternListProvider.getIndexPatternTags(pattern, $scope.defaultIndex === id);
return {
id: id,
title: pattern.get('title'),
url: kbnUrl.eval('#/management/kibana/indices/{{id}}', { id: id }),
class: 'sidebar-item-title ' + ($scope.editingId === id ? 'active' : ''),
default: $scope.defaultIndex === id
active: $scope.editingId === id,
default: $scope.defaultIndex === id,
tag: tags && tags.length ? tags[0] : null,
};
});
});
}).sort((a, b) => {
if(a.default) {
return -1;
}
if(b.default) {
return 1;
}
if(a.title < b.title) {
return -1;
}
if(a.title > b.title) {
return 1;
}
return 0;
}) || [];
updateIndexPatternList($scope, indexPatternCreationOptions, $scope.defaultIndex, $scope.indexPatternList);
};
$scope.$on('$destroy', destroyIndexPatternList);
$scope.editingId = $route.current.params.indexPatternId;
$scope.$watch('defaultIndex', () => renderList());
config.bindToScope($scope, 'defaultIndex');
$scope.$apply();
}
};
});

View file

@ -0,0 +1,15 @@
#indexPatternListReact {
.indexPatternList__headerWrapper {
padding-bottom: $euiSizeS;
}
.euiButtonEmpty__content {
justify-content: left;
padding: 0;
span {
text-overflow: ellipsis;
overflow: hidden;
}
}
}

View file

@ -0,0 +1,129 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
EuiButton,
EuiPopover,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiDescriptionList,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
} from '@elastic/eui';
export class CreateButton extends Component {
constructor(props) {
super(props);
this.state = {
isPopoverOpen: false,
};
}
static propTypes = {
options: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
description: PropTypes.string,
onClick: PropTypes.func.isRequired,
})),
}
togglePopover = () => {
this.setState({
isPopoverOpen: !this.state.isPopoverOpen,
});
}
closePopover = () => {
this.setState({
isPopoverOpen: false,
});
}
render() {
const { options, children } = this.props;
const { isPopoverOpen } = this.state;
if(!options || !options.length) {
return null;
}
if(options.length === 1) {
return (
<EuiButton
data-test-subj="createIndexPatternButton"
fill={true}
size={'s'}
onClick={options[0].onClick}
>
{children}
</EuiButton>
);
}
const button = (
<EuiButton
data-test-subj="createIndexPatternButton"
fill={true}
size="s"
iconType="arrowDown"
iconSide="right"
onClick={this.togglePopover}
>
{children}
</EuiButton>
);
if(options.length > 1) {
return (
<EuiPopover
id="singlePanel"
button={button}
isOpen={isPopoverOpen}
closePopover={this.closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel
items={options.map(option => {
return (
<EuiContextMenuItem
key={option.text}
onClick={option.onClick}
data-test-subj={option.testSubj}
>
<EuiDescriptionList style={{ whiteSpace: 'nowrap' }}>
<EuiDescriptionListTitle>
{option.text}
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{option.description}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiContextMenuItem>
);
})}
/>
</EuiPopover>
);
}
}
}

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { CreateButton } from './create_button';

View file

@ -0,0 +1,35 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { CreateButton } from '../create_button';
import { I18nProvider, FormattedMessage } from '@kbn/i18n/react';
export const Header = ({
indexPatternCreationOptions
}) => (
<I18nProvider>
<CreateButton options={indexPatternCreationOptions}>
<FormattedMessage
id="kbn.management.indexPatternList.header.createIndexPatternButtonLabel"
defaultMessage="Create index pattern"
/>
</CreateButton>
</I18nProvider>
);

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { Header } from './header';

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { List } from './list';

View file

@ -0,0 +1,83 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import {
EuiButtonEmpty,
EuiBadge,
EuiCallOut,
EuiSpacer,
} from '@elastic/eui';
export class List extends Component {
static propTypes = {
indexPatterns: PropTypes.array,
defaultIndex: PropTypes.string,
}
renderList() {
const { indexPatterns } = this.props;
return indexPatterns && indexPatterns.length ? (
<div>
{
indexPatterns.map(pattern => {
return (
<div key={pattern.id} >
<EuiButtonEmpty size="xs" href={pattern.url} data-test-subj="indexPatternLink">
{pattern.default ? <Fragment><i aria-label="Default index pattern" className="fa fa-star" /> </Fragment> : ''}
{pattern.active ? <strong>{pattern.title}</strong> : pattern.title} {pattern.tag ? (
<Fragment key={pattern.tag.key}>
{<EuiBadge color={pattern.tag.color || 'primary'}>{pattern.tag.name}</EuiBadge> }
</Fragment>
) : null}
</EuiButtonEmpty>
<EuiSpacer size="xs"/>
</div>
);
})
}
</div>
) : null;
}
renderNoDefaultMessage() {
const { defaultIndex } = this.props;
return !defaultIndex ? (
<div className="indexPatternList__headerWrapper">
<EuiCallOut
color="warning"
size="s"
iconType="alert"
title="No default index pattern. You must select or create one to continue."
/>
</div>
) : null;
}
render() {
return (
<div>
{this.renderNoDefaultMessage()}
{this.renderList()}
</div>
);
}
}

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { IndexPatternList } from './index_pattern_list';

View file

@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import { Header } from './components/header';
import { List } from './components/list';
export const IndexPatternList = ({
indexPatternCreationOptions,
defaultIndex,
indexPatterns
}) => (
<Fragment>
<div className="indexPatternList__headerWrapper" data-test-subj="createIndexPatternParent">
<Header indexPatternCreationOptions={indexPatternCreationOptions} />
</div>
<List indexPatterns={indexPatterns} defaultIndex={defaultIndex} />
</Fragment>
);

View file

@ -53,15 +53,27 @@ function addJsonFieldToIndexPattern(target, sourceString, fieldName, indexName)
async function importIndexPattern(doc, indexPatterns, overwriteAll) {
// TODO: consolidate this is the code in create_index_pattern_wizard.js
const emptyPattern = await indexPatterns.get();
const { title, timeFieldName, fields, fieldFormatMap, sourceFilters } = doc._source;
const {
title,
timeFieldName,
fields,
fieldFormatMap,
sourceFilters,
type,
typeMeta,
} = doc._source;
const importedIndexPattern = {
id: doc._id,
title,
timeFieldName
timeFieldName,
};
if (type) {
importedIndexPattern.type = type;
}
addJsonFieldToIndexPattern(importedIndexPattern, fields, 'fields', title);
addJsonFieldToIndexPattern(importedIndexPattern, fieldFormatMap, 'fieldFormatMap', title);
addJsonFieldToIndexPattern(importedIndexPattern, sourceFilters, 'sourceFilters', title);
addJsonFieldToIndexPattern(importedIndexPattern, typeMeta, 'typeMeta', title);
Object.assign(emptyPattern, importedIndexPattern);
const newId = await emptyPattern.create(true, !overwriteAll);
@ -128,14 +140,11 @@ export async function resolveIndexPatternConflicts(
export async function saveObjects(objs, overwriteAll) {
let importCount = 0;
await awaitEachItemInParallel(
objs,
async obj => {
if (await saveObject(obj, overwriteAll)) {
importCount++;
}
await awaitEachItemInParallel(objs, async obj => {
if (await saveObject(obj, overwriteAll)) {
importCount++;
}
);
});
return importCount;
}
@ -143,12 +152,7 @@ export async function saveObject(obj, overwriteAll) {
return await obj.save({ confirmOverwrite: !overwriteAll });
}
export async function resolveSavedSearches(
savedSearches,
services,
indexPatterns,
overwriteAll
) {
export async function resolveSavedSearches(savedSearches, services, indexPatterns, overwriteAll) {
let importCount = 0;
await awaitEachItemInParallel(savedSearches, async searchDoc => {
const obj = await getSavedObject(searchDoc, services);
@ -163,12 +167,7 @@ export async function resolveSavedSearches(
return importCount;
}
export async function resolveSavedObjects(
savedObjects,
overwriteAll,
services,
indexPatterns
) {
export async function resolveSavedObjects(savedObjects, overwriteAll, services, indexPatterns) {
const docTypes = groupByType(savedObjects);
// Keep track of how many we actually import because the user
@ -177,19 +176,20 @@ export async function resolveSavedObjects(
// Keep a record of any objects which fail to import for unknown reasons.
const failedImports = [];
// Start with the index patterns since everything is dependent on them
await awaitEachItemInParallel(
docTypes.indexPatterns,
async indexPatternDoc => {
try {
const importedIndexPatternId = await importIndexPattern(indexPatternDoc, indexPatterns, overwriteAll);
if (importedIndexPatternId) {
importedObjectCount++;
}
} catch (error) {
failedImports.push({ indexPatternDoc, error });
await awaitEachItemInParallel(docTypes.indexPatterns, async indexPatternDoc => {
try {
const importedIndexPatternId = await importIndexPattern(
indexPatternDoc,
indexPatterns,
overwriteAll
);
if (importedIndexPatternId) {
importedObjectCount++;
}
} catch (error) {
failedImports.push({ indexPatternDoc, error });
}
);
});
// We want to do the same for saved searches, but we want to keep them separate because they need
// to be applied _first_ because other saved objects can be dependent on those saved searches existing

View file

@ -67,14 +67,16 @@ export function ArgValueSuggestionsProvider(Private, indexPatterns) {
const search = partial ? `${partial}*` : '*';
const resp = await savedObjectsClient.find({
type: 'index-pattern',
fields: ['title'],
fields: ['title', 'type'],
search: `${search}`,
search_fields: ['title'],
perPage: 25
});
return resp.savedObjects.map(savedObject => {
return { name: savedObject.attributes.title };
});
return resp.savedObjects
.filter(savedObject => !savedObject.get('type'))
.map(savedObject => {
return { name: savedObject.attributes.title };
});
},
metric: async function (partial, functionArgs) {
if (!partial || !partial.includes(':')) {

View file

@ -27,7 +27,7 @@ export const createFieldsForWildcardRoute = pre => ({
validate: {
query: Joi.object().keys({
pattern: Joi.string().required(),
meta_fields: Joi.array().items(Joi.string()).default([])
meta_fields: Joi.array().items(Joi.string()).default([]),
}).default()
},
handler(req, reply) {

View file

@ -32,7 +32,7 @@ export class IndexPatternsService {
* Get a list of field objects for an index pattern that may contain wildcards
*
* @param {Object} [options={}]
* @property {String} options.pattern The moment compatible time pattern
* @property {String} options.pattern The index pattern
* @property {Number} options.metaFields The list of underscore prefixed fields that should
* be left in the field list (all others are removed).
* @return {Promise<Array<Fields>>}

View file

@ -104,6 +104,11 @@ export const dateHistogramBucketAgg = new BucketAggType({
default: null,
write: _.noop,
},
{
name: 'useNormalizedEsInterval',
default: true,
write: _.noop,
},
{
name: 'interval',
type: 'optioned',
@ -124,8 +129,8 @@ export const dateHistogramBucketAgg = new BucketAggType({
write: function (agg, output, aggs) {
setBounds(agg, true);
agg.buckets.setInterval(getInterval(agg));
const interval = agg.buckets.getInterval();
const { useNormalizedEsInterval } = agg.params;
const interval = agg.buckets.getInterval(useNormalizedEsInterval);
output.bucketInterval = interval;
output.params.interval = interval.expression;

View file

@ -19,6 +19,8 @@
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import '../../validate_date_interval';
import chrome from 'ui/chrome';
import { BucketAggType } from './_bucket_agg_type';
@ -95,6 +97,12 @@ export const histogramBucketAgg = new BucketAggType({
min: _.get(resp, 'aggregations.minAgg.value'),
max: _.get(resp, 'aggregations.maxAgg.value')
});
})
.catch(() => {
toastNotifications.addWarning(i18n.translate('common.ui.aggTypes.histogram.missingMaxMinValuesWarning', {
// eslint-disable-next-line max-len
defaultMessage: 'Unable to retrieve max and min values to auto-scale histogram buckets. This may lead to poor visualization performance.'
}));
});
},
write: function (aggConfig, output) {

View file

@ -1,6 +1,6 @@
<div>
<div class="visEditorAgg__formRow--flex">
<div ng-if="agg.type.params.byName.order" class="form-group">
<div ng-if="agg.type.params.byName.order && aggParam.options" class="form-group">
<label for="visEditorOrderByOrder{{agg.id}}">Order</label>
<select
id="visEditorOrderByOrder{{agg.id}}"

View file

@ -49,6 +49,7 @@ describe('callClient', () => {
_flatten: () => ({}),
requestIsStopped: () => {},
getField: () => 'indexPattern',
getPreferredSearchStrategyId: () => undefined,
...overrideSource
};
@ -134,7 +135,7 @@ describe('callClient', () => {
});
});
it(`still resolves the promise in spite of the failure`, () => {
it(`still bubbles up the failure`, () => {
const searchRequestFail = createSearchRequest('fail', {
source: {
getField: () => ({ type: 'fail' }),
@ -144,22 +145,7 @@ describe('callClient', () => {
searchRequests = [ searchRequestFail ];
return callClient(searchRequests).then(results => {
expect(results).to.eql(undefined);
});
});
it(`calls the errorHandler provided to the searchRequest`, () => {
const errorHandlerSpy = sinon.spy();
const searchRequestFail = createSearchRequest('fail', {
source: {
getField: () => ({ type: 'fail' }),
},
}, errorHandlerSpy);
searchRequests = [ searchRequestFail ];
return callClient(searchRequests).then(() => {
sinon.assert.calledOnce(errorHandlerSpy);
expect(results).to.eql([{ error: new Error('Search failed') }]);
});
});
});
@ -185,20 +171,6 @@ describe('callClient', () => {
expect(whenAbortedSpy.callCount).to.be(1);
});
});
it(`calls searchRequest.handleFailure() with the SearchError that's thrown`, async () => {
esShouldError = true;
const searchRequest = createSearchRequest(1);
const handleFailureSpy = sinon.spy();
searchRequest.handleFailure = handleFailureSpy;
searchRequests = [ searchRequest ];
return callClient(searchRequests).then(() => {
sinon.assert.calledOnce(handleFailureSpy);
expect(handleFailureSpy.args[0][0].name).to.be('SearchError');
});
});
});
describe('aborting at different points in the request lifecycle:', () => {
@ -348,18 +320,24 @@ describe('callClient', () => {
searchRequestA = createSearchRequest('a', {
source: {
getField: () => ({ type: 'a' }),
getSearchStrategyForSearchRequest: () => {},
getPreferredSearchStrategyId: () => {},
},
});
searchRequestB = createSearchRequest('b', {
source: {
getField: () => ({ type: 'b' }),
getSearchStrategyForSearchRequest: () => {},
getPreferredSearchStrategyId: () => {},
},
});
searchRequestA2 = createSearchRequest('a2', {
source: {
getField: () => ({ type: 'a' }),
getSearchStrategyForSearchRequest: () => {},
getPreferredSearchStrategyId: () => {},
},
});
});

View file

@ -161,7 +161,7 @@ export function CallClientProvider(Private, Promise, es, config) {
return;
}
const segregatedResponses = await Promise.all(abortableSearches.map(({ searching }) => searching));
const segregatedResponses = await Promise.all(abortableSearches.map(({ searching }) => searching.catch((e) => [{ error: e }])));
// Assigning searchRequests to strategies means that the responses come back in a different
// order than the original searchRequests. So we'll put them back in order so that we can

View file

@ -20,6 +20,7 @@
import { toastNotifications } from '../../notify';
import { RequestFailure } from '../../errors';
import { RequestStatus } from './req_status';
import { SearchError } from '../search_strategy/search_error';
export function CallResponseHandlersProvider(Private, Promise) {
const ABORTED = RequestStatus.ABORTED;
@ -58,7 +59,7 @@ export function CallResponseHandlersProvider(Private, Promise) {
if (searchRequest.filterError(response)) {
return progress();
} else {
return searchRequest.handleFailure(new RequestFailure(null, response));
return searchRequest.handleFailure(response.error instanceof SearchError ? response.error : new RequestFailure(null, response));
}
}

View file

@ -73,7 +73,11 @@ export function FetchNowProvider(Private, Promise) {
return startRequests(searchRequests)
.then(function () {
replaceAbortedRequests();
return callClient(searchRequests);
return callClient(searchRequests)
.catch(() => {
// Silently swallow errors that result from search requests so the consumer can surface
// them as notifications instead of courier forcing fatal errors.
});
})
.then(function (responses) {
replaceAbortedRequests();

View file

@ -123,7 +123,8 @@ export function SearchRequestProvider(Promise) {
handleFailure(error) {
this.success = false;
this.resp = error && error.resp;
this.resp = error;
this.resp = (error && error.resp) || error;
return this.errorHandler(this, error);
}

View file

@ -18,3 +18,4 @@
*/
export * from './search_source';
export * from './search_strategy';

View file

@ -34,4 +34,5 @@ export {
hasSearchStategyForIndexPattern,
isDefaultTypeIndexPattern,
SearchError,
getSearchErrorType,
} from './search_strategy';

View file

@ -130,6 +130,7 @@ export function SearchSourceProvider(Promise, Private, config) {
constructor(initialFields) {
this._id = _.uniqueId('data_source');
this._searchStrategyId = undefined;
this._fields = parseInitialFields(initialFields);
this._parent = undefined;
@ -164,6 +165,14 @@ export function SearchSourceProvider(Promise, Private, config) {
* PUBLIC API
*****/
setPreferredSearchStrategyId(searchStrategyId) {
this._searchStrategyId = searchStrategyId;
}
getPreferredSearchStrategyId() {
return this._searchStrategyId;
}
setFields(newFields) {
this._fields = newFields;
return this;

View file

@ -33,24 +33,23 @@ function getAllFetchParams(searchRequests, Promise) {
}
async function serializeAllFetchParams(fetchParams, searchRequests, serializeFetchParams) {
const searcRequestsWithFetchParams = [];
const searchRequestsWithFetchParams = [];
const failedSearchRequests = [];
// Gather the fetch param responses from all the successful requests.
fetchParams.forEach((result, index) => {
if (result.resolved) {
searcRequestsWithFetchParams.push(result.resolved);
searchRequestsWithFetchParams.push(result.resolved);
} else {
const searchRequest = searchRequests[index];
// TODO: All strategies will need to implement this.
searchRequest.handleFailure(result.rejected);
failedSearchRequests.push(searchRequest);
}
});
return {
serializedFetchParams: await serializeFetchParams(searcRequestsWithFetchParams),
serializedFetchParams: await serializeFetchParams(searchRequestsWithFetchParams),
failedSearchRequests,
};
}

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { SearchError, getSearchErrorType } from './search_error';

View file

@ -25,4 +25,4 @@ export {
export { isDefaultTypeIndexPattern } from './is_default_type_index_pattern';
export { SearchError } from './search_error';
export { SearchError, getSearchErrorType } from './search_error';

View file

@ -19,5 +19,5 @@
export const isDefaultTypeIndexPattern = indexPattern => {
// Default index patterns don't have `type` defined.
return indexPattern.type == null;
return !indexPattern.type;
};

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export type SearchError = any;
export type getSearchErrorType = any;

View file

@ -18,13 +18,14 @@
*/
export class SearchError extends Error {
constructor({ status, title, message, path }) {
constructor({ status, title, message, path, type }) {
super(message);
this.name = 'SearchError';
this.status = status;
this.title = title;
this.message = message;
this.path = path;
this.type = type;
// captureStackTrace is only available in the V8 engine, so any browser using
// a different JS engine won't have access to this method.
@ -37,3 +38,10 @@ export class SearchError extends Error {
Object.setPrototypeOf(this, SearchError.prototype);
}
}
export function getSearchErrorType({ message }) {
const msg = message.toLowerCase();
if(msg.indexOf('unsupported query') > -1) {
return 'UNSUPPORTED_QUERY';
}
}

View file

@ -27,12 +27,31 @@ export const addSearchStrategy = searchStrategy => {
searchStrategies.push(searchStrategy);
};
const getSearchStrategy = indexPattern => {
const getSearchStrategyByViability = indexPattern => {
return searchStrategies.find(searchStrategy => {
return searchStrategy.isViable(indexPattern);
});
};
const getSearchStrategyById = searchStrategyId => {
return searchStrategies.find(searchStrategy => {
return searchStrategy.id === searchStrategyId;
});
};
const getSearchStrategyForSearchRequest = searchRequest => {
// Allow the searchSource to declare the correct strategy with which to execute its searches.
const preferredSearchStrategyId = searchRequest.source.getPreferredSearchStrategyId();
if (preferredSearchStrategyId != null) {
return getSearchStrategyById(preferredSearchStrategyId);
}
// Otherwise try to match it to a strategy.
const indexPattern = searchRequest.source.getField('index');
return getSearchStrategyByViability(indexPattern);
};
/**
* Build a structure like this:
*
@ -52,9 +71,7 @@ export const assignSearchRequestsToSearchStrategies = searchRequests => {
const searchStrategyById = {};
searchRequests.forEach(searchRequest => {
const indexPattern = searchRequest.source.getField('index');
const matchingSearchStrategy = getSearchStrategy(indexPattern);
const matchingSearchStrategy = getSearchStrategyForSearchRequest(searchRequest);
const { id } = matchingSearchStrategy;
let searchStrategyWithRequest = searchStrategyById[id];
@ -76,5 +93,5 @@ export const assignSearchRequestsToSearchStrategies = searchRequests => {
};
export const hasSearchStategyForIndexPattern = indexPattern => {
return Boolean(getSearchStrategy(indexPattern));
return Boolean(getSearchStrategyByViability(indexPattern));
};

View file

@ -45,22 +45,22 @@ describe('SearchStrategyRegistry', () => {
const searchRequest0 = {
id: 0,
source: { getField: () => 'b' },
source: { getField: () => 'b', getPreferredSearchStrategyId: () => {} },
};
const searchRequest1 = {
id: 1,
source: { getField: () => 'a' },
source: { getField: () => 'a', getPreferredSearchStrategyId: () => {} },
};
const searchRequest2 = {
id: 2,
source: { getField: () => 'a' },
source: { getField: () => 'a', getPreferredSearchStrategyId: () => {} },
};
const searchRequest3 = {
id: 3,
source: { getField: () => 'b' },
source: { getField: () => 'b', getPreferredSearchStrategyId: () => {} },
};
const searchRequests = [ searchRequest0, searchRequest1, searchRequest2, searchRequest3];

View file

@ -79,7 +79,9 @@ export function IndexPatternProvider(Private, config, Promise, confirmModalPromi
_deserialize(map = '{}') {
return _.mapValues(angular.fromJson(map), deserializeFieldFormatMap);
}
}
},
type: 'keyword',
typeMeta: 'json',
});
function serializeFieldFormatMap(flat, format, field) {

View file

@ -25,7 +25,10 @@ export function createFieldsFetcher(apiClient, config) {
return this.fetchForTimePattern(indexPattern.title, interval);
}
return this.fetchForWildcard(indexPattern.title);
return this.fetchForWildcard(indexPattern.title, {
type: indexPattern.type,
params: indexPattern.typeMeta && indexPattern.typeMeta.params,
});
}
fetchForTimePattern(indexPatternId) {
@ -36,10 +39,12 @@ export function createFieldsFetcher(apiClient, config) {
});
}
fetchForWildcard(indexPatternId) {
fetchForWildcard(indexPatternId, options = {}) {
return apiClient.getFieldsForWildcard({
pattern: indexPatternId,
metaFields: config.get('metaFields'),
type: options.type,
params: options.params || {},
});
}
}

View file

@ -85,6 +85,8 @@ export function createIndexPatternsApiClient($http, basePath) {
const {
pattern,
metaFields,
type,
params,
} = options;
const url = getUrl(['_fields_for_wildcard'], {
@ -92,7 +94,23 @@ export function createIndexPatternsApiClient($http, basePath) {
meta_fields: metaFields,
});
return request('GET', url).then(resp => resp.fields);
// Fetch fields normally, and then if the index pattern is a specific type,
// pass the retrieved field information to the type-specific fields API for
// further processing
return request('GET', url).then(resp => {
if(type) {
const typeUrl = getUrl([type, '_fields_for_wildcard'], {
pattern,
fields: resp.fields,
meta_fields: metaFields,
params: JSON.stringify(params),
});
return request('GET', typeUrl).then(typeResp => typeResp.fields);
} else {
return resp.fields;
}
});
}
}

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/index_patterns';
export const INDEX_ILLEGAL_CHARACTERS_VISIBLE = INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.concat(',');

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export {
INDEX_ILLEGAL_CHARACTERS_VISIBLE,
} from './constants';

View file

@ -17,42 +17,11 @@
* under the License.
*/
import { ManagementSection } from './section';
export {
PAGE_TITLE_COMPONENT,
PAGE_SUBTITLE_COMPONENT,
PAGE_FOOTER_COMPONENT,
} from '../../../core_plugins/kibana/public/management/sections/settings/components/default_component_registry';
export { registerSettingsComponent } from '../../../core_plugins/kibana/public/management/sections/settings/components/component_registry';
export { Field } from '../../../core_plugins/kibana/public/management/sections/settings/components/field/field';
export const management = new ManagementSection('management', {
display: 'Management'
});
// TODO: where should this live?
management.register('data', {
display: 'Connect Data',
order: 0
});
management.register('elasticsearch', {
display: 'Elasticsearch',
order: 20,
icon: 'logoElasticsearch'
});
management.register('kibana', {
display: 'Kibana',
order: 30,
icon: 'logoKibana',
});
management.register('logstash', {
display: 'Logstash',
order: 30,
icon: 'logoLogstash',
});
export { management } from './sections_register';

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import './register';
export { IndexPatternCreationFactory } from './index_pattern_creation';
export { IndexPatternCreationConfig } from './index_pattern_creation_config';
export { IndexPatternCreationConfigRegistry } from './index_pattern_creation_config_registry';

View file

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IndexPatternCreationConfigRegistry } from './index_pattern_creation_config_registry';
class IndexPatternCreation {
constructor(registry, httpClient, type) {
this._registry = registry;
this._allTypes = this._registry.inOrder.map(Plugin => new Plugin({ httpClient }));
this._setCurrentType(type);
}
_setCurrentType = (type) => {
const index = type ? this._registry.inOrder.findIndex(Plugin => Plugin.key === type) : -1;
this._currentType = index > -1 && this._allTypes[index] ? this._allTypes[index] : null;
}
getType = () => {
return this._currentType || null;
}
getIndexPatternCreationOptions = async (urlHandler) => {
const options = [];
await Promise.all(this._allTypes.map(async type => {
const option = type.getIndexPatternCreationOption ? await type.getIndexPatternCreationOption(urlHandler) : null;
if(option) {
options.push(option);
}
}));
return options;
}
}
export const IndexPatternCreationFactory = (Private, $http) => {
return (type = 'default') => {
const indexPatternCreationRegistry = Private(IndexPatternCreationConfigRegistry);
const indexPatternCreationProvider = new IndexPatternCreation(indexPatternCreationRegistry, $http, type);
return indexPatternCreationProvider;
};
};

View file

@ -0,0 +1,87 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
const indexPatternTypeName = i18n.translate('common.ui.management.editIndexPattern.createIndex.defaultTypeName',
{ defaultMessage: 'index pattern' });
const indexPatternButtonText = i18n.translate('common.ui.management.editIndexPattern.createIndex.defaultButtonText',
{ defaultMessage: 'Standard index pattern' });
const indexPatternButtonDescription = i18n.translate('common.ui.management.editIndexPattern.createIndex.defaultButtonDescription',
{ defaultMessage: 'Can perform full aggregations against any data' });
export class IndexPatternCreationConfig {
static key = 'default';
constructor({
type = undefined,
name = indexPatternTypeName,
showSystemIndices = true,
httpClient = null,
}) {
this.type = type;
this.name = name;
this.showSystemIndices = showSystemIndices;
this.httpClient = httpClient;
}
async getIndexPatternCreationOption(urlHandler) {
return {
text: indexPatternButtonText,
description: indexPatternButtonDescription,
testSubj: `createStandardIndexPatternButton`,
onClick: () => {
urlHandler('/management/kibana/index');
},
};
}
getIndexPatternType = () => {
return this.type;
}
getIndexPatternName = () => {
return this.name;
}
getShowSystemIndices = () => {
return this.showSystemIndices;
}
getIndexTags() {
return [];
}
checkIndicesForErrors = () => {
return undefined;
}
getIndexPatternMappings = () => {
return {};
}
renderPrompt = () => {
return null;
}
getFetchForWildcardOptions = () => {
return {};
}
}

View file

@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { uiRegistry } from 'ui/registry/_registry';
export const IndexPatternCreationConfigRegistry = uiRegistry({
name: 'indexPatternCreation',
index: ['name'],
order: ['order'],
});

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IndexPatternCreationConfig } from './index_pattern_creation_config';
import { IndexPatternCreationConfigRegistry } from './index_pattern_creation_config_registry';
IndexPatternCreationConfigRegistry.register(() => IndexPatternCreationConfig);

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import './register';
export { IndexPatternListFactory } from './index_pattern_list';
export { IndexPatternListConfig } from './index_pattern_list_config';
export { IndexPatternListConfigRegistry } from './index_pattern_list_config_registry';

View file

@ -0,0 +1,52 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IndexPatternListConfigRegistry } from './index_pattern_list_config_registry';
class IndexPatternList {
constructor(registry) {
this._plugins = registry.inOrder.map(Plugin => new Plugin());
}
getIndexPatternTags = (indexPattern) => {
return this._plugins.reduce((tags, plugin) => {
return plugin.getIndexPatternTags ? tags.concat(plugin.getIndexPatternTags(indexPattern)) : tags;
}, []);
}
getFieldInfo = (indexPattern, field) => {
return this._plugins.reduce((info, plugin) => {
return plugin.getFieldInfo ? info.concat(plugin.getFieldInfo(indexPattern, field)) : info;
}, []);
}
areScriptedFieldsEnabled = (indexPattern) => {
return this._plugins.every((plugin) => {
return plugin.areScriptedFieldsEnabled ? plugin.areScriptedFieldsEnabled(indexPattern) : true;
});
}
}
export const IndexPatternListFactory = (Private) => {
return function () {
const indexPatternListRegistry = Private(IndexPatternListConfigRegistry);
const indexPatternListProvider = new IndexPatternList(indexPatternListRegistry);
return indexPatternListProvider;
};
};

View file

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class IndexPatternListConfig {
static key = 'default';
getIndexPatternTags = () => {
return [];
}
getFieldInfo = () => {
return [];
}
areScriptedFieldsEnabled = () => {
return true;
}
}

View file

@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { uiRegistry } from 'ui/registry/_registry';
export const IndexPatternListConfigRegistry = uiRegistry({
name: 'indexPatternList',
index: ['name'],
order: ['order'],
});

View file

@ -0,0 +1,23 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IndexPatternListConfig } from './index_pattern_list_config';
import { IndexPatternListConfigRegistry } from './index_pattern_list_config_registry';
IndexPatternListConfigRegistry.register(() => IndexPatternListConfig);

View file

@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ManagementSection } from './section';
export const management = new ManagementSection('management', {
display: 'Management'
});
management.register('data', {
display: 'Connect Data',
order: 0
});
management.register('elasticsearch', {
display: 'Elasticsearch',
order: 20,
icon: 'logoElasticsearch',
});
management.register('kibana', {
display: 'Kibana',
order: 30,
icon: 'logoKibana',
});
management.register('logstash', {
display: 'Logstash',
order: 30,
icon: 'logoLogstash',
});

View file

@ -18,6 +18,7 @@
*/
import dateMath from '@kbn/datemath';
import { parseEsInterval } from 'ui/utils/parse_es_interval';
const unitsDesc = dateMath.unitsDesc;
const largeMax = unitsDesc.indexOf('M');
@ -30,7 +31,7 @@ const largeMax = unitsDesc.indexOf('M');
* @param {moment.duration} duration
* @return {object}
*/
export function calcEsInterval(duration) {
export function convertDurationToNormalizedEsInterval(duration) {
for (let i = 0; i < unitsDesc.length; i++) {
const unit = unitsDesc[i];
const val = duration.as(unit);
@ -59,3 +60,12 @@ export function calcEsInterval(duration) {
expression: ms + 'ms'
};
}
export function convertIntervalToEsInterval(interval) {
const { value, unit } = parseEsInterval(interval);
return {
value,
unit,
expression: interval,
};
}

View file

@ -22,7 +22,10 @@ import moment from 'moment';
import chrome from 'ui/chrome';
import { parseInterval } from '../utils/parse_interval';
import { calcAutoInterval } from './calc_auto_interval';
import { calcEsInterval } from './calc_es_interval';
import {
convertDurationToNormalizedEsInterval,
convertIntervalToEsInterval,
} from './calc_es_interval';
import { fieldFormats } from '../registry/field_formats';
const config = chrome.getUiSettingsClient();
@ -152,6 +155,10 @@ TimeBuckets.prototype.getDuration = function () {
* @param {object|string|moment.duration} input - see desc
*/
TimeBuckets.prototype.setInterval = function (input) {
// Preserve the original units because they're lost when the interval is converted to a
// moment duration object.
this.originalInterval = input;
let interval = input;
// selection object -> val
@ -215,7 +222,7 @@ TimeBuckets.prototype.setInterval = function (input) {
*
* @return {[type]} [description]
*/
TimeBuckets.prototype.getInterval = function () {
TimeBuckets.prototype.getInterval = function (useNormalizedEsInterval = true) {
const self = this;
const duration = self.getDuration();
return decorateInterval(maybeScaleInterval(readInterval()));
@ -253,7 +260,9 @@ TimeBuckets.prototype.getInterval = function () {
// append some TimeBuckets specific props to the interval
function decorateInterval(interval) {
const esInterval = calcEsInterval(interval);
const esInterval = useNormalizedEsInterval
? convertDurationToNormalizedEsInterval(interval)
: convertIntervalToEsInterval(self.originalInterval);
interval.esValue = esInterval.value;
interval.esUnit = esInterval.unit;
interval.expression = esInterval.expression;
@ -341,12 +350,12 @@ TimeBuckets.__cached__ = function (self) {
function cachedGetter(prop) {
return {
value: function cachedGetter() {
value: function cachedGetter(...rest) {
if (cache.hasOwnProperty(prop)) {
return cache[prop];
}
return cache[prop] = self[prop]();
return cache[prop] = self[prop](...rest);
}
};
}

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { parseEsInterval } from './parse_es_interval';
export { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error';
export { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error';

View file

@ -0,0 +1,44 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class InvalidEsCalendarIntervalError extends Error {
constructor(
public readonly interval: string,
public readonly value: number,
public readonly unit: string,
public readonly type: string
) {
super(`Invalid calendar interval: ${interval}, value must be 1`);
this.name = 'InvalidEsCalendarIntervalError';
this.value = value;
this.unit = unit;
this.type = type;
// captureStackTrace is only available in the V8 engine, so any browser using
// a different JS engine won't have access to this method.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, InvalidEsCalendarIntervalError);
}
// Babel doesn't support traditional `extends` syntax for built-in classes.
// https://babeljs.io/docs/en/caveats/#classes
Object.setPrototypeOf(this, InvalidEsCalendarIntervalError.prototype);
}
}

View file

@ -0,0 +1,35 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class InvalidEsIntervalFormatError extends Error {
constructor(public readonly interval: string) {
super(`Invalid interval format: ${interval}`);
this.name = 'InvalidEsIntervalFormatError';
// captureStackTrace is only available in the V8 engine, so any browser using
// a different JS engine won't have access to this method.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, InvalidEsIntervalFormatError);
}
// Babel doesn't support traditional `extends` syntax for built-in classes.
// https://babeljs.io/docs/en/caveats/#classes
Object.setPrototypeOf(this, InvalidEsIntervalFormatError.prototype);
}
}

View file

@ -17,6 +17,8 @@
* under the License.
*/
import { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error';
import { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error';
import { parseEsInterval } from './parse_es_interval';
describe('parseEsInterval', () => {
@ -39,16 +41,29 @@ describe('parseEsInterval', () => {
expect(parseEsInterval('7d')).toEqual({ value: 7, unit: 'd', type: 'fixed' });
});
it('should throw an error for intervals containing calendar unit and multiple value', () => {
expect(() => parseEsInterval('4w')).toThrowError();
expect(() => parseEsInterval('12M')).toThrowError();
expect(() => parseEsInterval('10y')).toThrowError();
it('should throw a InvalidEsCalendarIntervalError for intervals containing calendar unit and multiple value', () => {
const intervals = ['4w', '12M', '10y'];
expect.assertions(intervals.length);
intervals.forEach(interval => {
try {
parseEsInterval(interval);
} catch (error) {
expect(error instanceof InvalidEsCalendarIntervalError).toBe(true);
}
});
});
it('should throw an error for invalid interval formats', () => {
expect(() => parseEsInterval('1')).toThrowError();
expect(() => parseEsInterval('h')).toThrowError();
expect(() => parseEsInterval('0m')).toThrowError();
expect(() => parseEsInterval('0.5h')).toThrowError();
it('should throw a InvalidEsIntervalFormatError for invalid interval formats', () => {
const intervals = ['1', 'h', '0m', '0.5h'];
expect.assertions(intervals.length);
intervals.forEach(interval => {
try {
parseEsInterval(interval);
} catch (error) {
expect(error instanceof InvalidEsIntervalFormatError).toBe(true);
}
});
});
});

View file

@ -16,8 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import dateMath from '@kbn/datemath';
import { InvalidEsCalendarIntervalError } from './invalid_es_calendar_interval_error';
import { InvalidEsIntervalFormatError } from './invalid_es_interval_format_error';
const ES_INTERVAL_STRING_REGEX = new RegExp(
'^([1-9][0-9]*)\\s*(' + dateMath.units.join('|') + ')$'
);
@ -47,7 +51,7 @@ export function parseEsInterval(interval: string): { value: number; unit: string
.match(ES_INTERVAL_STRING_REGEX);
if (!matches) {
throw Error(`Invalid interval format: ${interval}`);
throw new InvalidEsIntervalFormatError(interval);
}
const value = matches && parseFloat(matches[1]);
@ -55,7 +59,7 @@ export function parseEsInterval(interval: string): { value: number; unit: string
const type = unit && dateMath.unitsMap[unit].type;
if (type === 'calendar' && value !== 1) {
throw Error(`Invalid calendar interval: ${interval}, value must be 1`);
throw new InvalidEsCalendarIntervalError(interval, value, unit, type);
}
return {

View file

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VisualizationRequestError should render according to snapshot 1`] = `
<div
class="visualize-error visualize-chart"
>
<div
class="euiText euiText--extraSmall visualize-request-error"
>
<div
class="euiTextColor euiTextColor--danger"
>
Request error
</div>
</div>
</div>
`;

View file

@ -65,7 +65,15 @@
display: flex;
align-items: center;
justify-content: center;
.top { align-self: flext-start; }
.top { align-self: flex-start; }
.item { }
.bottom { align-self: flext-end; }
.bottom { align-self: flex-end; }
}
/**
* 1. Prevent large request errors from overflowing the container
*/
.visualize-request-error {
max-width: 100%;
max-height: 100%;
}

View file

@ -79,6 +79,13 @@ describe('<Visualization/>', () => {
expect(wrapper.text()).toBe('No results found');
});
it('should display error message when there is a request error that should be shown and no data', () => {
const errorVis = { ...vis, requestError: { message: 'Request error' }, showRequestError: true };
const data = null;
const wrapper = render(<Visualization vis={errorVis} visData={data} listenOnChange={true} uiState={uiState} />);
expect(wrapper.text()).toBe('Request error');
});
it('should render chart when data is present', () => {
const wrapper = render(<Visualization vis={vis} visData={visData} uiState={uiState} listenOnChange={true} />);
expect(wrapper.text()).not.toBe('No results found');

View file

@ -25,6 +25,7 @@ import { memoizeLast } from '../../utils/memoize';
import { Vis } from '../../vis';
import { VisualizationChart } from './visualization_chart';
import { VisualizationNoResults } from './visualization_noresults';
import { VisualizationRequestError } from './visualization_requesterror';
import './visualization.less';
@ -37,6 +38,12 @@ function shouldShowNoResultsMessage(vis: Vis, visData: any): boolean {
return Boolean(requiresSearch && isZeroHits && shouldShowMessage);
}
function shouldShowRequestErrorMessage(vis: Vis, visData: any): boolean {
const requestError = get(vis, 'requestError');
const showRequestError = get(vis, 'showRequestError');
return Boolean(!visData && requestError && showRequestError);
}
interface VisualizationProps {
listenOnChange: boolean;
onInit?: () => void;
@ -63,10 +70,13 @@ export class Visualization extends React.Component<VisualizationProps> {
const { vis, visData, onInit, uiState } = this.props;
const noResults = this.showNoResultsMessage(vis, visData);
const requestError = shouldShowRequestErrorMessage(vis, visData);
return (
<div className="visualization">
{noResults ? (
{requestError ? (
<VisualizationRequestError onInit={onInit} error={vis.requestError} />
) : noResults ? (
<VisualizationNoResults onInit={onInit} />
) : (
<VisualizationChart vis={vis} visData={visData} onInit={onInit} uiState={uiState} />

View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { render } from 'enzyme';
import { VisualizationRequestError } from './visualization_requesterror';
describe('VisualizationRequestError', () => {
it('should render according to snapshot', () => {
const wrapper = render(<VisualizationRequestError error="Request error" />);
expect(wrapper).toMatchSnapshot();
});
it('should set html when error is an object', () => {
const wrapper = render(<VisualizationRequestError error={{ message: 'Request error' }} />);
expect(wrapper.text()).toBe('Request error');
});
it('should set html when error is a string', () => {
const wrapper = render(<VisualizationRequestError error="Request error" />);
expect(wrapper.text()).toBe('Request error');
});
});

View file

@ -0,0 +1,62 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { EuiText } from '@elastic/eui';
import React from 'react';
import { SearchError } from 'ui/courier';
import { dispatchRenderComplete } from '../../render_complete';
interface VisualizationRequestErrorProps {
onInit?: () => void;
error: SearchError | string;
}
export class VisualizationRequestError extends React.Component<VisualizationRequestErrorProps> {
private containerDiv = React.createRef<HTMLDivElement>();
public render() {
const { error } = this.props;
const errorMessage = (error && error.message) || error;
return (
<div className="visualize-error visualize-chart" ref={this.containerDiv}>
<EuiText className="visualize-request-error" color="danger" size="xs">
{errorMessage}
</EuiText>
</div>
);
}
public componentDidMount() {
this.afterRender();
}
public componentDidUpdate() {
this.afterRender();
}
private afterRender() {
if (this.props.onInit) {
this.props.onInit();
}
if (this.containerDiv.current) {
dispatchRenderComplete(this.containerDiv.current);
}
}
}

View file

@ -69,6 +69,8 @@ export class VisualizeDataLoader {
public async fetch(params: RequestHandlerParams): Promise<any> {
this.vis.filters = { timeRange: params.timeRange };
this.vis.requestError = undefined;
this.vis.showRequestError = false;
try {
// searchSource is only there for courier request handler
@ -95,6 +97,7 @@ export class VisualizeDataLoader {
} catch (e) {
params.searchSource.cancelQueued();
this.vis.requestError = e;
this.vis.showRequestError = e.type && e.type === 'UNSUPPORTED_QUERY';
if (isTermSizeZeroError(e)) {
return toastNotifications.addDanger(
`Your visualization ('${this.vis.title}') has an error: it has a term ` +

View file

@ -48,6 +48,7 @@ export {
navbarExtensions,
contextMenuActions,
managementSections,
indexManagement,
devTools,
docViews,
hacks,

Some files were not shown because too many files have changed in this diff Show more