mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Add configs for terminate_after (#37643)
This commit is contained in:
parent
3184be10e0
commit
4d88aaa274
12 changed files with 166 additions and 60 deletions
|
@ -128,6 +128,14 @@ store saved searches, visualizations and dashboards. Kibana creates a new index
|
|||
if the index doesn’t already exist. If you configure a custom index, the name must
|
||||
be lowercase, and conform to {es} {ref}/indices-create-index.html[index name limitations].
|
||||
|
||||
`kibana.autocompleteTimeout:`:: *Default: "1000"* Time in milliseconds to wait
|
||||
for autocomplete suggestions from Elasticsearch. This value must be a whole number
|
||||
greater than zero.
|
||||
|
||||
`kibana.autocompleteTerminateAfter:`:: *Default: "100000"* Maximum number of
|
||||
documents loaded by each shard to generate autocomplete suggestions. This value
|
||||
must be a whole number greater than zero.
|
||||
|
||||
`logging.dest:`:: *Default: `stdout`* Enables you specify a file where Kibana
|
||||
stores log output.
|
||||
|
||||
|
|
|
@ -1,5 +1,34 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders control with warning 1`] = `
|
||||
<EuiFormRow
|
||||
data-test-subj="inputControl0"
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
id="controlId"
|
||||
label={
|
||||
<React.Fragment>
|
||||
<EuiToolTip
|
||||
content="This is a warning"
|
||||
delay="regular"
|
||||
position="top"
|
||||
>
|
||||
<EuiIcon
|
||||
type="alert"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
test control
|
||||
</React.Fragment>
|
||||
}
|
||||
labelType="label"
|
||||
>
|
||||
<div>
|
||||
My Control
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
`;
|
||||
|
||||
exports[`renders disabled control with tooltip 1`] = `
|
||||
<EuiFormRow
|
||||
data-test-subj="inputControl0"
|
||||
|
|
|
@ -20,10 +20,7 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFormRow, EuiToolTip, EuiIcon } from '@elastic/eui';
|
||||
|
||||
export function FormRow(props) {
|
||||
let control = props.children;
|
||||
|
@ -35,9 +32,20 @@ export function FormRow(props) {
|
|||
);
|
||||
}
|
||||
|
||||
const label = props.warningMsg ? (
|
||||
<>
|
||||
<EuiToolTip position="top" content={props.warningMsg}>
|
||||
<EuiIcon type="alert" />
|
||||
</EuiToolTip>
|
||||
{props.label}
|
||||
</>
|
||||
) : (
|
||||
props.label
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={props.label}
|
||||
label={label}
|
||||
id={props.id}
|
||||
data-test-subj={'inputControl' + props.controlIndex}
|
||||
>
|
||||
|
@ -48,6 +56,7 @@ export function FormRow(props) {
|
|||
|
||||
FormRow.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
warningMsg: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
controlIndex: PropTypes.number.isRequired,
|
||||
|
|
|
@ -37,6 +37,20 @@ test('renders enabled control', () => {
|
|||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
||||
});
|
||||
|
||||
test('renders control with warning', () => {
|
||||
const component = shallow(
|
||||
<FormRow
|
||||
label="test control"
|
||||
id="controlId"
|
||||
controlIndex={0}
|
||||
warningMsg="This is a warning"
|
||||
>
|
||||
<div>My Control</div>
|
||||
</FormRow>
|
||||
);
|
||||
expect(component).toMatchSnapshot(); // eslint-disable-line
|
||||
});
|
||||
|
||||
test('renders disabled control with tooltip', () => {
|
||||
const component = shallow(
|
||||
<FormRow
|
||||
|
|
|
@ -66,6 +66,7 @@ export class InputControlVis extends Component {
|
|||
formatOptionLabel={control.format}
|
||||
disableMsg={control.isEnabled() ? null : control.disabledReason}
|
||||
multiselect={control.options.multiselect}
|
||||
partialResults={control.partialResults}
|
||||
dynamicOptions={control.options.dynamicOptions}
|
||||
controlIndex={index}
|
||||
stageFilter={this.props.stageFilter}
|
||||
|
|
|
@ -23,33 +23,30 @@ import _ from 'lodash';
|
|||
import { FormRow } from './form_row';
|
||||
import { injectI18n } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiComboBox,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFieldText, EuiComboBox } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
class ListControlUi extends Component {
|
||||
|
||||
state = {
|
||||
isLoading: false
|
||||
}
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
this._isMounted = true;
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this._isMounted = false;
|
||||
}
|
||||
};
|
||||
|
||||
handleOnChange = (selectedOptions) => {
|
||||
handleOnChange = selectedOptions => {
|
||||
const selectedValues = selectedOptions.map(({ value }) => {
|
||||
return value;
|
||||
});
|
||||
this.props.stageFilter(this.props.controlIndex, selectedValues);
|
||||
}
|
||||
};
|
||||
|
||||
debouncedFetch = _.debounce(async (searchValue) => {
|
||||
debouncedFetch = _.debounce(async searchValue => {
|
||||
await this.props.fetchOptions(searchValue);
|
||||
|
||||
if (this._isMounted) {
|
||||
|
@ -59,11 +56,14 @@ class ListControlUi extends Component {
|
|||
}
|
||||
}, 300);
|
||||
|
||||
onSearchChange = (searchValue) => {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
}, this.debouncedFetch.bind(null, searchValue));
|
||||
}
|
||||
onSearchChange = searchValue => {
|
||||
this.setState(
|
||||
{
|
||||
isLoading: true,
|
||||
},
|
||||
this.debouncedFetch.bind(null, searchValue)
|
||||
);
|
||||
};
|
||||
|
||||
renderControl() {
|
||||
const { intl } = this.props;
|
||||
|
@ -73,7 +73,7 @@ class ListControlUi extends Component {
|
|||
<EuiFieldText
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'inputControl.vis.listControl.selectTextPlaceholder',
|
||||
defaultMessage: 'Select...'
|
||||
defaultMessage: 'Select...',
|
||||
})}
|
||||
disabled={true}
|
||||
/>
|
||||
|
@ -85,7 +85,7 @@ class ListControlUi extends Component {
|
|||
return {
|
||||
label: this.props.formatOptionLabel(option).toString(),
|
||||
value: option,
|
||||
['data-test-subj']: `option_${option.toString().replace(' ', '_')}`
|
||||
['data-test-subj']: `option_${option.toString().replace(' ', '_')}`,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
|
@ -103,7 +103,7 @@ class ListControlUi extends Component {
|
|||
<EuiComboBox
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'inputControl.vis.listControl.selectPlaceholder',
|
||||
defaultMessage: 'Select...'
|
||||
defaultMessage: 'Select...',
|
||||
})}
|
||||
options={options}
|
||||
isLoading={this.state.isLoading}
|
||||
|
@ -118,10 +118,18 @@ class ListControlUi extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const partialResultsWarningMessage = i18n.translate(
|
||||
'inputControl.vis.listControl.partialResultsWarningMessage',
|
||||
{
|
||||
defaultMessage: `Terms list is incomplete. Adjust the autocomplete settings in kibana.yml for more results.`,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<FormRow
|
||||
id={this.props.id}
|
||||
label={this.props.label}
|
||||
warningMsg={this.props.partialResults ? partialResultsWarningMessage : undefined}
|
||||
controlIndex={this.props.controlIndex}
|
||||
disableMsg={this.props.disableMsg}
|
||||
>
|
||||
|
@ -140,6 +148,7 @@ ListControlUi.propTypes = {
|
|||
disableMsg: PropTypes.string,
|
||||
multiselect: PropTypes.bool,
|
||||
dynamicOptions: PropTypes.bool,
|
||||
partialResults: PropTypes.bool,
|
||||
controlIndex: PropTypes.number.isRequired,
|
||||
stageFilter: PropTypes.func.isRequired,
|
||||
fetchOptions: PropTypes.func,
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
import { PhraseFilterManager } from './filter_manager/phrase_filter_manager';
|
||||
import { createSearchSource } from './create_search_source';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import chrome from 'ui/chrome';
|
||||
|
||||
function getEscapedQuery(query = '') {
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators
|
||||
|
@ -98,8 +99,8 @@ class ListControl extends Control {
|
|||
|
||||
const fieldName = this.filterManager.fieldName;
|
||||
const initialSearchSourceState = {
|
||||
timeout: '1s',
|
||||
terminate_after: 100000
|
||||
timeout: `${chrome.getInjected('autocompleteTimeout')}ms`,
|
||||
terminate_after: chrome.getInjected('autocompleteTerminateAfter')
|
||||
};
|
||||
const aggs = termsAgg({
|
||||
field: indexPattern.fields.byName[fieldName],
|
||||
|
@ -141,6 +142,7 @@ class ListControl extends Control {
|
|||
return;
|
||||
}
|
||||
|
||||
this.partialResults = resp.terminated_early || resp.timed_out;
|
||||
this.selectOptions = selectOptions;
|
||||
this.enable = true;
|
||||
this.disabledReason = '';
|
||||
|
|
|
@ -17,8 +17,16 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { listControlFactory } from './list_control_factory';
|
||||
|
||||
chrome.getInjected.mockImplementation((key) => {
|
||||
switch(key) {
|
||||
case 'autocompleteTimeout': return 1000;
|
||||
case 'autocompleteTerminateAfter': return 100000;
|
||||
}
|
||||
});
|
||||
|
||||
const mockField = {
|
||||
name: 'myField',
|
||||
format: {
|
||||
|
@ -59,7 +67,7 @@ function MockSearchSource() {
|
|||
};
|
||||
}
|
||||
|
||||
const mockKbnApi = {
|
||||
const getMockKbnApi = () => ({
|
||||
indexPatterns: {
|
||||
get: async () => {
|
||||
return mockIndexPattern;
|
||||
|
@ -73,8 +81,8 @@ const mockKbnApi = {
|
|||
return [];
|
||||
}
|
||||
},
|
||||
SearchSource: MockSearchSource,
|
||||
};
|
||||
SearchSource: jest.fn(MockSearchSource),
|
||||
});
|
||||
|
||||
describe('hasValue', () => {
|
||||
const controlParams = {
|
||||
|
@ -86,7 +94,7 @@ describe('hasValue', () => {
|
|||
|
||||
let listControl;
|
||||
beforeEach(async () => {
|
||||
listControl = await listControlFactory(controlParams, mockKbnApi, useTimeFilter);
|
||||
listControl = await listControlFactory(controlParams, getMockKbnApi(), useTimeFilter);
|
||||
});
|
||||
|
||||
test('should be false when control has no value', () => {
|
||||
|
@ -111,12 +119,22 @@ describe('fetch', () => {
|
|||
options: {}
|
||||
};
|
||||
const useTimeFilter = false;
|
||||
let mockKbnApi;
|
||||
|
||||
let listControl;
|
||||
beforeEach(async () => {
|
||||
mockKbnApi = getMockKbnApi();
|
||||
listControl = await listControlFactory(controlParams, mockKbnApi, useTimeFilter);
|
||||
});
|
||||
|
||||
test('should pass in timeout parameters from injected vars', async () => {
|
||||
await listControl.fetch();
|
||||
expect(mockKbnApi.SearchSource).toHaveBeenCalledWith({
|
||||
timeout: `1000ms`,
|
||||
terminate_after: 100000
|
||||
});
|
||||
});
|
||||
|
||||
test('should set selectOptions to results of terms aggregation', async () => {
|
||||
await listControl.fetch();
|
||||
expect(listControl.selectOptions).toEqual(['Zurich Airport', 'Xi an Xianyang International Airport']);
|
||||
|
@ -135,6 +153,7 @@ describe('fetch with ancestors', () => {
|
|||
let listControl;
|
||||
let parentControl;
|
||||
beforeEach(async () => {
|
||||
const mockKbnApi = getMockKbnApi();
|
||||
listControl = await listControlFactory(controlParams, mockKbnApi, useTimeFilter);
|
||||
|
||||
const parentControlParams = {
|
||||
|
|
|
@ -54,6 +54,9 @@ export default function (kibana) {
|
|||
defaultAppId: Joi.string().default('home'),
|
||||
index: Joi.string().default('.kibana'),
|
||||
disableWelcomeScreen: Joi.boolean().default(false),
|
||||
autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000),
|
||||
// TODO Also allow units here like in elasticsearch config once this is moved to the new platform
|
||||
autocompleteTimeout: Joi.number().integer().min(1).default(1000),
|
||||
}).default();
|
||||
},
|
||||
|
||||
|
@ -76,25 +79,27 @@ export default function (kibana) {
|
|||
{
|
||||
id: 'kibana:discover',
|
||||
title: i18n.translate('kbn.discoverTitle', {
|
||||
defaultMessage: 'Discover'
|
||||
defaultMessage: 'Discover',
|
||||
}),
|
||||
order: -1003,
|
||||
url: `${kbnBaseUrl}#/discover`,
|
||||
icon: 'plugins/kibana/assets/discover.svg',
|
||||
euiIconType: 'discoverApp',
|
||||
}, {
|
||||
},
|
||||
{
|
||||
id: 'kibana:visualize',
|
||||
title: i18n.translate('kbn.visualizeTitle', {
|
||||
defaultMessage: 'Visualize'
|
||||
defaultMessage: 'Visualize',
|
||||
}),
|
||||
order: -1002,
|
||||
url: `${kbnBaseUrl}#/visualize`,
|
||||
icon: 'plugins/kibana/assets/visualize.svg',
|
||||
euiIconType: 'visualizeApp',
|
||||
}, {
|
||||
},
|
||||
{
|
||||
id: 'kibana:dashboard',
|
||||
title: i18n.translate('kbn.dashboardTitle', {
|
||||
defaultMessage: 'Dashboard'
|
||||
defaultMessage: 'Dashboard',
|
||||
}),
|
||||
order: -1001,
|
||||
url: `${kbnBaseUrl}#/dashboards`,
|
||||
|
@ -106,25 +111,27 @@ export default function (kibana) {
|
|||
subUrlBase: `${kbnBaseUrl}#/dashboard`,
|
||||
icon: 'plugins/kibana/assets/dashboard.svg',
|
||||
euiIconType: 'dashboardApp',
|
||||
}, {
|
||||
},
|
||||
{
|
||||
id: 'kibana:dev_tools',
|
||||
title: i18n.translate('kbn.devToolsTitle', {
|
||||
defaultMessage: 'Dev Tools'
|
||||
defaultMessage: 'Dev Tools',
|
||||
}),
|
||||
order: 9001,
|
||||
url: '/app/kibana#/dev_tools',
|
||||
icon: 'plugins/kibana/assets/wrench.svg',
|
||||
euiIconType: 'devToolsApp',
|
||||
}, {
|
||||
},
|
||||
{
|
||||
id: 'kibana:management',
|
||||
title: i18n.translate('kbn.managementTitle', {
|
||||
defaultMessage: 'Management'
|
||||
defaultMessage: 'Management',
|
||||
}),
|
||||
order: 9003,
|
||||
url: `${kbnBaseUrl}#/management`,
|
||||
icon: 'plugins/kibana/assets/settings.svg',
|
||||
euiIconType: 'managementApp',
|
||||
linkToLastSubUrl: false
|
||||
linkToLastSubUrl: false,
|
||||
},
|
||||
],
|
||||
|
||||
|
@ -268,7 +275,7 @@ export default function (kibana) {
|
|||
},
|
||||
advancedSettings: {
|
||||
show: true,
|
||||
save: true
|
||||
save: true,
|
||||
},
|
||||
indexPatterns: {
|
||||
save: true,
|
||||
|
@ -288,7 +295,7 @@ export default function (kibana) {
|
|||
index_patterns: true,
|
||||
objects: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -323,6 +330,6 @@ export default function (kibana) {
|
|||
server.expose('systemApi', systemApi);
|
||||
server.expose('handleEsError', handleEsError);
|
||||
server.injectUiAppVars('kibana', () => injectVars(server));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -29,7 +29,9 @@ export function injectVars(server) {
|
|||
// Get types that are import and exportable, by default yes unless isImportableAndExportable is set to false
|
||||
const { types: allTypes } = server.savedObjects;
|
||||
const savedObjectsManagement = server.getSavedObjectsManagement();
|
||||
const importAndExportableTypes = allTypes.filter(type => savedObjectsManagement.isImportAndExportable(type));
|
||||
const importAndExportableTypes = allTypes.filter(type =>
|
||||
savedObjectsManagement.isImportAndExportable(type)
|
||||
);
|
||||
|
||||
return {
|
||||
kbnDefaultAppId: serverConfig.get('kibana.defaultAppId'),
|
||||
|
@ -37,6 +39,8 @@ export function injectVars(server) {
|
|||
regionmapsConfig: regionmap,
|
||||
mapConfig: mapConfig,
|
||||
importAndExportableTypes,
|
||||
autocompleteTerminateAfter: serverConfig.get('kibana.autocompleteTerminateAfter'),
|
||||
autocompleteTimeout: serverConfig.get('kibana.autocompleteTimeout'),
|
||||
tilemapsConfig: {
|
||||
deprecated: {
|
||||
isOverridden: isOverridden,
|
||||
|
|
|
@ -21,6 +21,9 @@ import { get, map } from 'lodash';
|
|||
import handleESError from '../../../lib/handle_es_error';
|
||||
|
||||
export function registerValueSuggestions(server) {
|
||||
const serverConfig = server.config();
|
||||
const autocompleteTerminateAfter = serverConfig.get('kibana.autocompleteTerminateAfter');
|
||||
const autocompleteTimeout = serverConfig.get('kibana.autocompleteTimeout');
|
||||
server.route({
|
||||
path: '/api/kibana/suggestions/values/{index}',
|
||||
method: ['POST'],
|
||||
|
@ -28,7 +31,11 @@ export function registerValueSuggestions(server) {
|
|||
const { index } = req.params;
|
||||
const { field, query, boolFilter } = req.payload;
|
||||
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
|
||||
const body = getBody({ field, query, boolFilter });
|
||||
const body = getBody(
|
||||
{ field, query, boolFilter },
|
||||
autocompleteTerminateAfter,
|
||||
autocompleteTimeout
|
||||
);
|
||||
try {
|
||||
const response = await callWithRequest(req, 'search', { index, body });
|
||||
const buckets = get(response, 'aggregations.suggestions.buckets') || [];
|
||||
|
@ -37,30 +44,26 @@ export function registerValueSuggestions(server) {
|
|||
} catch (error) {
|
||||
throw handleESError(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getBody({ field, query, boolFilter = [] }) {
|
||||
function getBody({ field, query, boolFilter = [] }, terminateAfter, timeout) {
|
||||
// Helps ensure that the regex is not evaluated eagerly against the terms dictionary
|
||||
const executionHint = 'map';
|
||||
|
||||
// Helps keep the number of buckets that need to be tracked at the shard level contained in case
|
||||
// this is a high cardinality field
|
||||
const terminateAfter = 100000;
|
||||
|
||||
// We don't care about the accuracy of the counts, just the content of the terms, so this reduces
|
||||
// the amount of information that needs to be transmitted to the coordinating node
|
||||
const shardSize = 10;
|
||||
|
||||
return {
|
||||
size: 0,
|
||||
timeout: '1s',
|
||||
timeout: `${timeout}ms`,
|
||||
terminate_after: terminateAfter,
|
||||
query: {
|
||||
bool: {
|
||||
filter: boolFilter,
|
||||
}
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
suggestions: {
|
||||
|
@ -68,14 +71,14 @@ function getBody({ field, query, boolFilter = [] }) {
|
|||
field,
|
||||
include: `${getEscapedQuery(query)}.*`,
|
||||
execution_hint: executionHint,
|
||||
shard_size: shardSize
|
||||
}
|
||||
}
|
||||
}
|
||||
shard_size: shardSize,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getEscapedQuery(query = '') {
|
||||
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators
|
||||
return query.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, (match) => `\\${match}`);
|
||||
return query.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, match => `\\${match}`);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,8 @@ export function serializeFetchParams(
|
|||
const body = {
|
||||
...fetchParams.body || {},
|
||||
};
|
||||
if (esShardTimeout > 0) {
|
||||
|
||||
if (!('timeout' in body) && esShardTimeout > 0) {
|
||||
body.timeout = `${esShardTimeout}ms`;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue