diff --git a/src/ui/public/utils/parse_es_interval/index.js b/src/ui/public/utils/parse_es_interval/index.js new file mode 100644 index 0000000000000..38ce09487a1d8 --- /dev/null +++ b/src/ui/public/utils/parse_es_interval/index.js @@ -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 { ParseEsIntervalInvalidCalendarIntervalError } from './parse_es_interval_invalid_calendar_interval_error'; +export { ParseEsIntervalInvalidFormatError } from './parse_es_interval_invalid_format_error'; diff --git a/src/ui/public/utils/parse_es_interval.test.ts b/src/ui/public/utils/parse_es_interval/parse_es_interval.test.ts similarity index 68% rename from src/ui/public/utils/parse_es_interval.test.ts rename to src/ui/public/utils/parse_es_interval/parse_es_interval.test.ts index 05b29a5fb69e7..56014da5fea7b 100644 --- a/src/ui/public/utils/parse_es_interval.test.ts +++ b/src/ui/public/utils/parse_es_interval/parse_es_interval.test.ts @@ -18,6 +18,8 @@ */ import { parseEsInterval } from './parse_es_interval'; +import { ParseEsIntervalInvalidCalendarIntervalError } from './parse_es_interval_invalid_calendar_interval_error'; +import { ParseEsIntervalInvalidFormatError } from './parse_es_interval_invalid_format_error'; describe('parseEsInterval', () => { it('should correctly parse an interval containing unit and single value', () => { @@ -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 ParseEsIntervalInvalidCalendarIntervalError 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 ParseEsIntervalInvalidCalendarIntervalError).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 ParseEsIntervalInvalidFormatError 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 ParseEsIntervalInvalidFormatError).toBe(true); + } + }); }); }); diff --git a/src/ui/public/utils/parse_es_interval.ts b/src/ui/public/utils/parse_es_interval/parse_es_interval.ts similarity index 87% rename from src/ui/public/utils/parse_es_interval.ts rename to src/ui/public/utils/parse_es_interval/parse_es_interval.ts index 984f7e278d4fb..718e6b408d3a9 100644 --- a/src/ui/public/utils/parse_es_interval.ts +++ b/src/ui/public/utils/parse_es_interval/parse_es_interval.ts @@ -16,8 +16,12 @@ * specific language governing permissions and limitations * under the License. */ + import dateMath from '@kbn/datemath'; +import { ParseEsIntervalInvalidCalendarIntervalError } from './parse_es_interval_invalid_calendar_interval_error'; +import { ParseEsIntervalInvalidFormatError } from './parse_es_interval_invalid_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 ParseEsIntervalInvalidFormatError(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 ParseEsIntervalInvalidCalendarIntervalError(interval); } return { diff --git a/src/ui/public/utils/parse_es_interval/parse_es_interval_invalid_calendar_interval_error.ts b/src/ui/public/utils/parse_es_interval/parse_es_interval_invalid_calendar_interval_error.ts new file mode 100644 index 0000000000000..80da8178e409d --- /dev/null +++ b/src/ui/public/utils/parse_es_interval/parse_es_interval_invalid_calendar_interval_error.ts @@ -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 ParseEsIntervalInvalidCalendarIntervalError extends Error { + constructor(public readonly interval: string) { + super(`Invalid calendar interval: ${interval}, value must be 1`); + this.name = 'ParseEsIntervalInvalidCalendarIntervalError'; + + // 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, ParseEsIntervalInvalidCalendarIntervalError); + } + + // Babel doesn't support traditional `extends` syntax for built-in classes. + // https://babeljs.io/docs/en/caveats/#classes + Object.setPrototypeOf(this, ParseEsIntervalInvalidCalendarIntervalError.prototype); + } +} diff --git a/src/ui/public/utils/parse_es_interval/parse_es_interval_invalid_format_error.ts b/src/ui/public/utils/parse_es_interval/parse_es_interval_invalid_format_error.ts new file mode 100644 index 0000000000000..78521b8821293 --- /dev/null +++ b/src/ui/public/utils/parse_es_interval/parse_es_interval_invalid_format_error.ts @@ -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 ParseEsIntervalInvalidFormatError extends Error { + constructor(public readonly interval: string) { + super(`Invalid interval format: ${interval}`); + this.name = 'ParseEsIntervalInvalidFormatError'; + + // 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, ParseEsIntervalInvalidFormatError); + } + + // Babel doesn't support traditional `extends` syntax for built-in classes. + // https://babeljs.io/docs/en/caveats/#classes + Object.setPrototypeOf(this, ParseEsIntervalInvalidFormatError.prototype); + } +} diff --git a/x-pack/plugins/rollup/public/crud_app/index.js b/x-pack/plugins/rollup/public/crud_app/index.js index 8d8be11bbc6bb..1b9d0d92461c6 100644 --- a/x-pack/plugins/rollup/public/crud_app/index.js +++ b/x-pack/plugins/rollup/public/crud_app/index.js @@ -13,7 +13,7 @@ import { management } from 'ui/management'; import routes from 'ui/routes'; import { CRUD_APP_BASE_PATH } from './constants'; -import { setHttpClient } from './services'; +import { setHttp } from './services'; import { App } from './app'; import template from './main.html'; import { rollupJobsStore } from './store'; @@ -28,22 +28,6 @@ esSection.register('rollup_jobs', { url: `#${CRUD_APP_BASE_PATH}`, }); -export const manageAngularLifecycle = ($scope, $route, elem) => { - const lastRoute = $route.current; - - const deregister = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - if (lastRoute.$$route.template === currentRoute.$$route.template) { - $route.current = lastRoute; - } - }); - - $scope.$on('$destroy', () => { - deregister && deregister(); - elem && unmountComponentAtNode(elem); - }); -}; - const renderReact = async (elem) => { render( @@ -61,15 +45,27 @@ routes.when(`${CRUD_APP_BASE_PATH}/:view?`, { template: template, controllerAs: 'rollupJobs', controller: class IndexRollupJobsController { - constructor($scope, $route, $http) { + constructor($scope, $route, $injector) { // NOTE: We depend upon Angular's $http service because it's decorated with interceptors, // e.g. to check license status per request. - setHttpClient($http); + setHttp($injector.get('$http')); $scope.$$postDigest(() => { - const elem = document.getElementById('rollupJobsReactRoot'); - renderReact(elem); - manageAngularLifecycle($scope, $route, elem); + const appElement = document.getElementById('rollupJobsReactRoot'); + renderReact(appElement); + + const lastRoute = $route.current; + const stopListeningForLocationChange = $scope.$on('$locationChangeSuccess', () => { + const currentRoute = $route.current; + if (lastRoute.$$route.template === currentRoute.$$route.template) { + $route.current = lastRoute; + } + }); + + $scope.$on('$destroy', () => { + stopListeningForLocationChange(); + unmountComponentAtNode(appElement); + }); }); } } diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js index e9ec51458829b..51c257d568b31 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/job_create.js @@ -6,7 +6,9 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; -import { mapValues, cloneDeep } from 'lodash'; +import mapValues from 'lodash/object/mapValues'; +import cloneDeep from 'lodash/lang/cloneDeep'; +import debounce from 'lodash/function/debounce'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { @@ -24,10 +26,14 @@ import { } from '@elastic/eui'; import { CRUD_APP_BASE_PATH } from '../../constants'; -import { getRouterLinkProps } from '../../services'; +import { + getRouterLinkProps, + validateIndexPattern, +} from '../../services'; import { Navigation } from './navigation'; import { StepLogistics } from './step_logistics'; +import { StepDateHistogram } from './step_date_histogram'; import { STEP_LOGISTICS, STEP_DATE_HISTOGRAM, @@ -64,8 +70,38 @@ export class JobCreateUi extends Component { nextStepId: stepIds[1], previousStepId: undefined, stepsFieldErrors: this.getStepsFieldsErrors(stepsFields), + stepsFieldErrorsAsync: {}, + areStepErrorsVisible: false, stepsFields, + isValidatingIndexPattern: false, + indexPatternAsyncErrors: undefined, + indexPatternTimeFields: [], }; + + this.lastIndexPatternValidationTime = 0; + } + + componentDidUpdate(prevProps, prevState) { + const indexPattern = this.getIndexPattern(); + if (indexPattern !== this.getIndexPattern(prevState)) { + + // If the user hasn't entered anything, then skip validation. + if (!indexPattern || !indexPattern.trim()) { + this.setState({ + indexPatternAsyncErrors: undefined, + indexPatternTimeFields: [], + isValidatingIndexPattern: false, + }); + + return; + } + + this.setState({ + isValidatingIndexPattern: true, + }); + + this.requestIndexPatternValidation(); + } } componentWillUnmount() { @@ -73,6 +109,70 @@ export class JobCreateUi extends Component { this.props.clearCreateJobErrors(); } + requestIndexPatternValidation = debounce(() => { + const indexPattern = this.getIndexPattern(); + + const lastIndexPatternValidationTime = this.lastIndexPatternValidationTime = Date.now(); + validateIndexPattern(indexPattern).then(response => { + // Ignore all responses except that to the most recent request. + if (lastIndexPatternValidationTime !== this.lastIndexPatternValidationTime) { + return; + } + + const { + doesMatchIndices: doesIndexPatternMatchIndices, + doesMatchRollupIndices: doesIndexPatternMatchRollupIndices, + timeFields: indexPatternTimeFields, + } = response.data; + + let indexPatternAsyncErrors; + + if (doesIndexPatternMatchRollupIndices) { + indexPatternAsyncErrors = [( + + )]; + } else if (!doesIndexPatternMatchIndices) { + indexPatternAsyncErrors = [( + + )]; + } else if (!indexPatternTimeFields.length) { + indexPatternAsyncErrors = [( + + )]; + } + + this.setState({ + indexPatternAsyncErrors, + indexPatternTimeFields, + isValidatingIndexPattern: false, + }); + + // Select first time field by default. + this.onFieldsChange({ + dateHistogramField: indexPatternTimeFields.length ? indexPatternTimeFields[0] : null, + }, STEP_DATE_HISTOGRAM); + }).catch(() => { + // Ignore all responses except that to the most recent request. + if (lastIndexPatternValidationTime !== this.lastIndexPatternValidationTime) { + return; + } + + // TODO: Show toast or inline error. + this.setState({ + isValidatingIndexPattern: false, + }); + }); + }, 300); + getSteps() { const { currentStepId, checkpointStepId } = this.state; const indexOfCurrentStep = stepIds.indexOf(currentStepId); @@ -104,7 +204,7 @@ export class JobCreateUi extends Component { // error. if (!this.canGoToStep(stepId)) { this.setState({ - showStepErrors: true, + areStepErrorsVisible: true, }); return; } @@ -115,7 +215,7 @@ export class JobCreateUi extends Component { currentStepId: stepId, nextStepId: stepIds[currentStepIndex + 1], previousStepId: stepIds[currentStepIndex - 1], - showStepErrors: false, + areStepErrorsVisible: false, isSaving: false, }); @@ -211,6 +311,10 @@ export class JobCreateUi extends Component { }; } + getIndexPattern(state = this.state) { + return state.stepsFields[STEP_LOGISTICS].indexPattern; + } + save = () => { const { createJob } = this.props; const jobConfig = this.getAllFields(); @@ -307,7 +411,16 @@ export class JobCreateUi extends Component { } renderCurrentStep() { - const { currentStepId, stepsFields, stepsFieldErrors, showStepErrors } = this.state; + const { + currentStepId, + stepsFields, + stepsFieldErrors, + areStepErrorsVisible, + isValidatingIndexPattern, + indexPatternTimeFields, + indexPatternAsyncErrors, + } = this.state; + const currentStepFields = stepsFields[currentStepId]; const currentStepFieldErrors = stepsFieldErrors[currentStepId]; @@ -318,7 +431,21 @@ export class JobCreateUi extends Component { fields={currentStepFields} onFieldsChange={this.onFieldsChange} fieldErrors={currentStepFieldErrors} - showStepErrors={showStepErrors} + areStepErrorsVisible={areStepErrorsVisible} + isValidatingIndexPattern={isValidatingIndexPattern} + indexPatternAsyncErrors={indexPatternAsyncErrors} + /> + ); + + case STEP_DATE_HISTOGRAM: + return ( + ); @@ -328,16 +455,20 @@ export class JobCreateUi extends Component { } renderNavigation() { - const { nextStepId, previousStepId } = this.state; + const { nextStepId, previousStepId, areStepErrorsVisible } = this.state; const { isSaving } = this.props; + const hasNextStep = nextStepId != null; + // Users can click the next step button as long as validation hasn't executed. + const canGoToNextStep = hasNextStep && (!areStepErrorsVisible || this.canGoToStep(nextStepId)); return ( ); diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/navigation/navigation.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/navigation/navigation.js index 505ecb846c234..94814bd0b5957 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/navigation/navigation.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/navigation/navigation.js @@ -23,6 +23,7 @@ export const Navigation = ({ goToNextStep, goToPreviousStep, save, + canGoToNextStep, }) => { if (isSaving) { return ( @@ -60,6 +61,7 @@ export const Navigation = ({ iconType="arrowRight" iconSide="right" onClick={goToNextStep} + isDisabled={!canGoToNextStep} fill > Next @@ -90,10 +92,11 @@ export const Navigation = ({ }; Navigation.propTypes = { - hasNextStep: PropTypes.bool, - hasPreviousStep: PropTypes.bool, - isSaving: PropTypes.bool, + hasNextStep: PropTypes.bool.isRequired, + hasPreviousStep: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, goToNextStep: PropTypes.func, goToPreviousStep: PropTypes.func, - save: PropTypes.func, + save: PropTypes.func.isRequired, + canGoToNextStep: PropTypes.bool.isRequired, }; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/step_date_histogram/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/step_date_histogram/index.js new file mode 100644 index 0000000000000..13376d8cf9636 --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/step_date_histogram/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { StepDateHistogram } from './step_date_histogram'; diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/step_date_histogram/step_date_histogram.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/step_date_histogram/step_date_histogram.js new file mode 100644 index 0000000000000..d3798cff22488 --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/step_date_histogram/step_date_histogram.js @@ -0,0 +1,323 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButtonEmpty, + EuiCallOut, + EuiDescribedFormGroup, + EuiCode, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiLink, + EuiSelect, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { + dateHistogramDetailsUrl, + dateHistogramAggregationUrl, +} from '../../../services'; + +export class StepDateHistogramUi extends Component { + static propTypes = { + fields: PropTypes.object.isRequired, + onFieldsChange: PropTypes.func.isRequired, + fieldErrors: PropTypes.object.isRequired, + areStepErrorsVisible: PropTypes.bool.isRequired, + timeFields: PropTypes.array.isRequired, + } + + static getDerivedStateFromProps(props) { + const { timeFields } = props; + + const dateHistogramFieldOptions = timeFields.map(timeField => ({ + value: timeField, + text: timeField, + })); + + return { dateHistogramFieldOptions }; + } + + constructor(props) { + super(props); + + this.state = { + isLoadingTimeFields: true, + dateHistogramFieldOptions: [], + }; + } + + render() { + const { + fields, + onFieldsChange, + areStepErrorsVisible, + fieldErrors, + } = this.props; + + const { + dateHistogramInterval, + dateHistogramDelay, + dateHistogramTimeZone, + dateHistogramField, + } = fields; + + const { + dateHistogramInterval: errorDateHistogramInterval, + dateHistogramDelay: errorDateHistogramDelay, + dateHistogramTimeZone: errorDateHistogramTimeZone, + dateHistogramField: errorDateHistogramField, + } = fieldErrors; + + const { + dateHistogramFieldOptions, + } = this.state; + + return ( + + + + +

+ +

+
+ + +

+ + + + ), + }} + /> +

+
+
+ + + + + + +
+ + + + + +

+ +

+ + )} + description={( + + )} + fullWidth + > + + )} + error={errorDateHistogramField} + isInvalid={Boolean(areStepErrorsVisible && errorDateHistogramField)} + > + onFieldsChange({ dateHistogramField: e.target.value })} + /> + + + + )} + error={errorDateHistogramInterval} + isInvalid={Boolean(areStepErrorsVisible && errorDateHistogramInterval)} + helpText={( + +

+ +

+
+ )} + > + onFieldsChange({ dateHistogramInterval: e.target.value })} + isInvalid={Boolean(areStepErrorsVisible && errorDateHistogramInterval)} + /> +
+
+ + +

+ +

+ + )} + description={( + + )} + fullWidth + > + + )} + error={errorDateHistogramDelay} + isInvalid={Boolean(areStepErrorsVisible && errorDateHistogramDelay)} + helpText={( + +

+ +

+
+ )} + > + onFieldsChange({ dateHistogramDelay: e.target.value })} + isInvalid={Boolean(areStepErrorsVisible && errorDateHistogramDelay)} + /> +
+ + + )} + error={errorDateHistogramTimeZone || ''} + isInvalid={Boolean(areStepErrorsVisible && errorDateHistogramTimeZone)} + helpText={( + + + + ), + }} + /> + )} + > + onFieldsChange({ dateHistogramTimeZone: e.target.value })} + isInvalid={Boolean(areStepErrorsVisible && errorDateHistogramTimeZone)} + /> + +
+
+ + {this.renderErrors()} +
+ ); + } + + renderErrors = () => { + const { areStepErrorsVisible } = this.props; + + if (!areStepErrorsVisible) { + return null; + } + + return ( + + + + )} + color="danger" + iconType="cross" + /> + + ); + } +} + +export const StepDateHistogram = injectI18n(StepDateHistogramUi); diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/step_logistics/step_logistics.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/step_logistics/step_logistics.js index 0d22f57f3dd12..fa42bf2903a5a 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/step_logistics/step_logistics.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/step_logistics/step_logistics.js @@ -5,11 +5,13 @@ */ import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiCallOut, + EuiDescribedFormGroup, EuiFieldNumber, EuiFieldText, EuiFlexGroup, @@ -19,7 +21,6 @@ import { EuiSpacer, EuiText, EuiTitle, - EuiDescribedFormGroup, } from '@elastic/eui'; import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/index_patterns'; @@ -27,12 +28,23 @@ import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices'; import { logisticalDetailsUrl } from '../../../services'; export class StepLogisticsUi extends Component { + static propTypes = { + fields: PropTypes.object.isRequired, + onFieldsChange: PropTypes.func.isRequired, + fieldErrors: PropTypes.object.isRequired, + areStepErrorsVisible: PropTypes.bool.isRequired, + isValidatingIndexPattern: PropTypes.bool.isRequired, + indexPatternAsyncErrors: PropTypes.array, + } + render() { const { fields, onFieldsChange, - showStepErrors, + areStepErrorsVisible, fieldErrors, + isValidatingIndexPattern, + indexPatternAsyncErrors, } = this.props; const { @@ -117,11 +129,11 @@ export class StepLogisticsUi extends Component { /> )} error={errorId} - isInvalid={Boolean(showStepErrors && errorId)} + isInvalid={Boolean(areStepErrorsVisible && errorId)} fullWidth > onFieldsChange({ id: e.target.value })} fullWidth @@ -155,8 +167,8 @@ export class StepLogisticsUi extends Component { defaultMessage="Index pattern" /> )} - error={errorIndexPattern} - isInvalid={Boolean(showStepErrors && errorIndexPattern)} + error={errorIndexPattern || indexPatternAsyncErrors} + isInvalid={Boolean((areStepErrorsVisible && errorIndexPattern)) || Boolean(indexPatternAsyncErrors)} helpText={(

@@ -180,7 +192,8 @@ export class StepLogisticsUi extends Component { onFieldsChange({ indexPattern: e.target.value })} - isInvalid={Boolean(showStepErrors && errorIndexPattern)} + isInvalid={Boolean(areStepErrorsVisible && errorIndexPattern) || Boolean(indexPatternAsyncErrors)} + isLoading={isValidatingIndexPattern} fullWidth /> @@ -193,7 +206,7 @@ export class StepLogisticsUi extends Component { /> )} error={errorRollupIndex} - isInvalid={Boolean(showStepErrors && errorRollupIndex)} + isInvalid={Boolean(areStepErrorsVisible && errorRollupIndex)} helpText={( onFieldsChange({ rollupIndex: e.target.value })} - isInvalid={Boolean(showStepErrors && errorRollupIndex)} + isInvalid={Boolean(areStepErrorsVisible && errorRollupIndex)} fullWidth /> @@ -242,13 +255,13 @@ export class StepLogisticsUi extends Component { /> )} error={errorRollupCron} - isInvalid={Boolean(showStepErrors && errorRollupCron)} + isInvalid={Boolean(areStepErrorsVisible && errorRollupCron)} fullWidth > onFieldsChange({ rollupCron: e.target.value })} - isInvalid={Boolean(showStepErrors && errorRollupCron)} + isInvalid={Boolean(areStepErrorsVisible && errorRollupCron)} fullWidth /> @@ -261,13 +274,13 @@ export class StepLogisticsUi extends Component { /> )} error={errorRollupPageSize} - isInvalid={Boolean(showStepErrors && errorRollupPageSize)} + isInvalid={Boolean(areStepErrorsVisible && errorRollupPageSize)} fullWidth > onFieldsChange({ rollupPageSize: e.target.value })} - isInvalid={Boolean(showStepErrors && errorRollupPageSize)} + isInvalid={Boolean(areStepErrorsVisible && errorRollupPageSize)} fullWidth /> @@ -280,9 +293,9 @@ export class StepLogisticsUi extends Component { } renderErrors = () => { - const { showStepErrors } = this.props; + const { areStepErrorsVisible } = this.props; - if (!showStepErrors) { + if (!areStepErrorsVisible) { return null; } diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js index 99f4b068adb15..dea0c5aca04a4 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js @@ -9,6 +9,9 @@ import { validateIndexPattern } from './validate_index_pattern'; import { validateRollupIndex } from './validate_rollup_index'; import { validateRollupCron } from './validate_rollup_cron'; import { validateRollupPageSize } from './validate_rollup_page_size'; +import { validateDateHistogramField } from './validate_date_histogram_field'; +import { validateDateHistogramInterval } from './validate_date_histogram_interval'; +import { validateDateHistogramDelay } from './validate_date_histogram_delay'; export const STEP_LOGISTICS = 'STEP_LOGISTICS'; export const STEP_DATE_HISTOGRAM = 'STEP_DATE_HISTOGRAM'; @@ -44,8 +47,8 @@ export const stepIdToStepConfigMap = { const errors = { id: validateId(id), - indexPattern: validateIndexPattern(indexPattern), - rollupIndex: validateRollupIndex(rollupIndex), + indexPattern: validateIndexPattern(indexPattern, rollupIndex), + rollupIndex: validateRollupIndex(rollupIndex, indexPattern), rollupCron: validateRollupCron(rollupCron), rollupPageSize: validateRollupPageSize(rollupPageSize), }; @@ -55,10 +58,25 @@ export const stepIdToStepConfigMap = { }, [STEP_DATE_HISTOGRAM]: { defaultFields: { - dateHistogramInterval: '1h', + dateHistogramField: null, + dateHistogramInterval: null, dateHistogramDelay: null, - dateHistogramTimeZone: 'UTC', - dateHistogramField: 'utc_time', + dateHistogramTimeZone: null, + }, + fieldsValidator: fields => { + const { + dateHistogramField, + dateHistogramInterval, + dateHistogramDelay, + } = fields; + + const errors = { + dateHistogramField: validateDateHistogramField(dateHistogramField), + dateHistogramInterval: validateDateHistogramInterval(dateHistogramInterval), + dateHistogramDelay: validateDateHistogramDelay(dateHistogramDelay), + }; + + return errors; }, }, [STEP_GROUPS]: { diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_delay.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_delay.js new file mode 100644 index 0000000000000..02b334711772f --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_delay.js @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + ParseEsIntervalInvalidFormatError, + ParseEsIntervalInvalidCalendarIntervalError, + parseEsInterval, +} from 'ui/utils/parse_es_interval'; + +export function validateDateHistogramDelay(dateHistogramDelay) { + // This field is optional, so if nothing has been provided we can skip validation. + if (!dateHistogramDelay || !dateHistogramDelay.trim()) { + return undefined; + } + + try { + parseEsInterval(dateHistogramDelay); + } catch(error) { + if (error instanceof ParseEsIntervalInvalidFormatError) { + return [( + + )]; + } + + if (error instanceof ParseEsIntervalInvalidCalendarIntervalError) { + return [( + + )]; + } + + throw(error); + } + + return undefined; +} diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_field.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_field.js new file mode 100644 index 0000000000000..2e1c2628f1054 --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_field.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export function validateDateHistogramField(dateHistogramField) { + if (!dateHistogramField || !dateHistogramField.trim()) { + return [( + + )]; + } + + return undefined; +} diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_interval.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_interval.js new file mode 100644 index 0000000000000..91ae62efc1563 --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_date_histogram_interval.js @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + ParseEsIntervalInvalidFormatError, + ParseEsIntervalInvalidCalendarIntervalError, + parseEsInterval, +} from 'ui/utils/parse_es_interval'; + +export function validateDateHistogramInterval(dateHistogramInterval) { + if (!dateHistogramInterval || !dateHistogramInterval.trim()) { + return [( + + )]; + } + + try { + parseEsInterval(dateHistogramInterval); + } catch(error) { + if (error instanceof ParseEsIntervalInvalidFormatError) { + return [( + + )]; + } + + if (error instanceof ParseEsIntervalInvalidCalendarIntervalError) { + return [( + + )]; + } + + throw(error); + } + + return undefined; +} diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_index_pattern.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_index_pattern.js index 051f3244c1629..97a9bccc70623 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_index_pattern.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/validate_index_pattern.js @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/index_patterns'; -export function validateIndexPattern(indexPattern) { +export function validateIndexPattern(indexPattern, rollupIndex) { if (!indexPattern || !indexPattern.trim()) { return [( + )]; + } + const illegalCharacters = INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => { if (indexPattern.includes(char)) { chars.push(char); @@ -28,7 +37,7 @@ export function validateIndexPattern(indexPattern) { }, []); if (illegalCharacters.length) { - return[( + return [( + )]; + } + const illegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.reduce((chars, char) => { if (rollupIndex.includes(char)) { chars.push(char); diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js index 974657e42fe52..8bb658b05c267 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js @@ -259,6 +259,7 @@ export class DetailPanelUi extends Component { onClose={closeDetailPanel} aria-labelledby="rollupJobDetailsFlyoutTitle" size="m" + maxWidth={400} > diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js index 0d741f8b304ed..5e7d900481c7c 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js @@ -119,7 +119,6 @@ export class JobTableUi extends Component { pageChanged: PropTypes.func.isRequired, pageSizeChanged: PropTypes.func.isRequired, sortChanged: PropTypes.func.isRequired, - dispatch: PropTypes.func.isRequired, } static defaultProps = { diff --git a/x-pack/plugins/rollup/public/crud_app/services/api.js b/x-pack/plugins/rollup/public/crud_app/services/api.js index aaaa4dee356c4..cbd0913637c85 100644 --- a/x-pack/plugins/rollup/public/crud_app/services/api.js +++ b/x-pack/plugins/rollup/public/crud_app/services/api.js @@ -5,36 +5,35 @@ */ import chrome from 'ui/chrome'; - -let httpClient; - -export const setHttpClient = (client) => { - httpClient = client; -}; +import { getHttp } from './http_provider'; const apiPrefix = chrome.addBasePath('/api/rollup'); export async function loadJobs() { - const { data: { jobs } } = await httpClient.get(`${apiPrefix}/jobs`); + const { data: { jobs } } = await getHttp().get(`${apiPrefix}/jobs`); return jobs; } export async function startJobs(jobIds) { const body = { jobIds }; - return await httpClient.post(`${apiPrefix}/start`, body); + return await getHttp().post(`${apiPrefix}/start`, body); } export async function stopJobs(jobIds) { const body = { jobIds }; - return await httpClient.post(`${apiPrefix}/stop`, body); + return await getHttp().post(`${apiPrefix}/stop`, body); } export async function deleteJobs(jobIds) { const body = { jobIds }; - return await httpClient.post(`${apiPrefix}/delete`, body); + return await getHttp().post(`${apiPrefix}/delete`, body); } export async function createJob(job) { const body = { job }; - return await httpClient.put(`${apiPrefix}/create`, body); + return await getHttp().put(`${apiPrefix}/create`, body); +} + +export async function validateIndexPattern(indexPattern) { + return await getHttp().get(`${apiPrefix}/index_pattern_validity/${indexPattern}`); } diff --git a/x-pack/plugins/rollup/public/crud_app/services/documentation_links.js b/x-pack/plugins/rollup/public/crud_app/services/documentation_links.js index f1cf9b094f498..eebaec9ecc28e 100644 --- a/x-pack/plugins/rollup/public/crud_app/services/documentation_links.js +++ b/x-pack/plugins/rollup/public/crud_app/services/documentation_links.js @@ -7,4 +7,8 @@ import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; + export const logisticalDetailsUrl = `${esBase}/rollup-job-config.html#_logistical_details`; +export const dateHistogramDetailsUrl = `${esBase}/rollup-job-config.html#_date_histogram_2`; + +export const dateHistogramAggregationUrl = `${esBase}/search-aggregations-bucket-datehistogram-aggregation.html`; diff --git a/x-pack/plugins/rollup/public/crud_app/services/http_provider.js b/x-pack/plugins/rollup/public/crud_app/services/http_provider.js new file mode 100644 index 0000000000000..835a7bdc09d86 --- /dev/null +++ b/x-pack/plugins/rollup/public/crud_app/services/http_provider.js @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// This is an Angular service, which is why we use this provider pattern to access it within +// our React app. +let _http; + +export function setHttp(http) { + _http = http; +} + +export function getHttp() { + return _http; +} diff --git a/x-pack/plugins/rollup/public/crud_app/services/index.js b/x-pack/plugins/rollup/public/crud_app/services/index.js index 6013870340fba..4e90e69313d9b 100644 --- a/x-pack/plugins/rollup/public/crud_app/services/index.js +++ b/x-pack/plugins/rollup/public/crud_app/services/index.js @@ -8,11 +8,16 @@ export { createJob, deleteJobs, loadJobs, - setHttpClient, startJobs, stopJobs, + validateIndexPattern, } from './api'; +export { + setHttp, + getHttp, +} from './http_provider'; + export { sortTable } from './sort_table'; export { filterItems } from './filter_items'; export { flattenPanelTree } from './flatten_panel_tree'; @@ -31,6 +36,8 @@ export { export { logisticalDetailsUrl, + dateHistogramDetailsUrl, + dateHistogramAggregationUrl, } from './documentation_links'; export { diff --git a/x-pack/plugins/rollup/public/crud_app/store/action_types.js b/x-pack/plugins/rollup/public/crud_app/store/action_types.js index b7998ec850caf..a2117016977f2 100644 --- a/x-pack/plugins/rollup/public/crud_app/store/action_types.js +++ b/x-pack/plugins/rollup/public/crud_app/store/action_types.js @@ -19,9 +19,6 @@ export const CREATE_JOB_SUCCESS = 'CREATE_JOB_SUCCESS'; export const CREATE_JOB_FAILURE = 'CREATE_JOB_FAILURE'; export const CLEAR_CREATE_JOB_ERRORS = 'CLEAR_CREATE_JOB_ERRORS'; -// Delete jobs -export const DELETE_JOBS_SUCCESS = 'DELETE_JOBS_SUCCESS'; - // Table state export const FILTER_CHANGED = 'FILTER_CHANGED'; export const PAGE_CHANGED = 'PAGE_CHANGED'; diff --git a/x-pack/plugins/rollup/public/crud_app/store/actions/delete_jobs.js b/x-pack/plugins/rollup/public/crud_app/store/actions/delete_jobs.js index 80f73f796c22a..5b50045e0102a 100644 --- a/x-pack/plugins/rollup/public/crud_app/store/actions/delete_jobs.js +++ b/x-pack/plugins/rollup/public/crud_app/store/actions/delete_jobs.js @@ -7,7 +7,6 @@ import { toastNotifications } from 'ui/notify'; import { deleteJobs as sendDeleteJobsRequest } from '../../services'; -import { DELETE_JOBS_SUCCESS } from '../action_types'; import { getDetailPanelJob } from '../selectors'; import { loadJobs } from './load_jobs'; @@ -20,9 +19,11 @@ export const deleteJobs = (jobIds) => async (dispatch, getState) => { return toastNotifications.addDanger(error.data.message); } - dispatch({ - type: DELETE_JOBS_SUCCESS, - }); + if (jobIds.length === 1) { + toastNotifications.addSuccess(`Rollup job '${jobIds[0]}' was deleted`); + } else { + toastNotifications.addSuccess(`${jobIds.length} rollup jobs were deleted`); + } dispatch(loadJobs()); diff --git a/x-pack/plugins/rollup/server/client/elasticsearch_rollup.js b/x-pack/plugins/rollup/server/client/elasticsearch_rollup.js index 28452e3b4a0f9..aaaf6450b04d8 100644 --- a/x-pack/plugins/rollup/server/client/elasticsearch_rollup.js +++ b/x-pack/plugins/rollup/server/client/elasticsearch_rollup.js @@ -10,7 +10,7 @@ export const elasticsearchJsPlugin = (Client, config, components) => { Client.prototype.rollup = components.clientAction.namespaceFactory(); const rollup = Client.prototype.rollup.prototype; - rollup.capabilitiesByRollupIndex = ca({ + rollup.rollupIndexCapabilities = ca({ urls: [ { fmt: '<%=indexPattern%>/_xpack/rollup/data', @@ -24,36 +24,33 @@ export const elasticsearchJsPlugin = (Client, config, components) => { method: 'GET' }); - rollup.capabilitiesByIndex = ca({ + rollup.search = ca({ urls: [ { - fmt: '/_xpack/rollup/data/<%=indices%>', + fmt: '/<%=index%>/_rollup_search', req: { - indices: { + index: { type: 'string' } } - }, - { - fmt: '/_xpack/rollup/data/', } ], - method: 'GET' + needBody: true, + method: 'POST' }); - rollup.search = ca({ + rollup.fieldCapabilities = ca({ urls: [ { - fmt: '/<%=index%>/_rollup_search', + fmt: '/<%=indexPattern%>/_field_caps?fields=*', req: { - index: { + indexPattern: { type: 'string' } } } ], - needBody: true, - method: 'POST' + method: 'GET' }); rollup.jobs = ca({ diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns.js b/x-pack/plugins/rollup/server/routes/api/index_patterns.js index 1d0af4662f8e5..3c690d213d991 100644 --- a/x-pack/plugins/rollup/server/routes/api/index_patterns.js +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns.js @@ -48,7 +48,7 @@ export function registerFieldsForWildcardRoute(server) { const rollupFields = []; const rollupFieldNames = []; const fieldsFromFieldCapsApi = indexBy(await getFieldCapabilities(callWithRequest, [rollupIndex], metaFields), 'name'); - const rollupIndexCapabilities = getCapabilitiesForRollupIndices(await callWithRequest('rollup.capabilitiesByRollupIndex', { + const rollupIndexCapabilities = getCapabilitiesForRollupIndices(await callWithRequest('rollup.rollupIndexCapabilities', { indexPattern: rollupIndex }))[rollupIndex].aggs; diff --git a/x-pack/plugins/rollup/server/routes/api/indices.js b/x-pack/plugins/rollup/server/routes/api/indices.js index 252bb1abb1e54..794d098665cc0 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices.js +++ b/x-pack/plugins/rollup/server/routes/api/indices.js @@ -9,13 +9,13 @@ import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; import { licensePreRoutingFactory } from'../../lib/license_pre_routing_factory'; import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities'; -/** - * Returns a list of all rollup index names - */ export function registerIndicesRoute(server) { const isEsError = isEsErrorFactory(server); const licensePreRouting = licensePreRoutingFactory(server); + /** + * Returns a list of all rollup index names + */ server.route({ path: '/api/rollup/indices', method: 'GET', @@ -25,7 +25,7 @@ export function registerIndicesRoute(server) { handler: async (request, reply) => { const callWithRequest = callWithRequestFactory(server, request); try { - const data = await callWithRequest('rollup.capabilitiesByRollupIndex', { + const data = await callWithRequest('rollup.rollupIndexCapabilities', { indexPattern: '_all' }); reply(getCapabilitiesForRollupIndices(data)); @@ -37,4 +37,61 @@ export function registerIndicesRoute(server) { } } }); + + /** + * Returns information on validiity of an index pattern for creating a rollup job: + * - Does the index pattern match any indices? + * - Does the index pattern match rollup indices? + * - Which time fields are available in the matching indices? + */ + server.route({ + path: '/api/rollup/index_pattern_validity/{indexPattern}', + method: 'GET', + config: { + pre: [ licensePreRouting ] + }, + handler: async (request, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + try { + const { indexPattern } = request.params; + const [ fieldCapabilities, rollupIndexCapabilities ] = await Promise.all([ + callWithRequest('rollup.fieldCapabilities', { indexPattern }), + callWithRequest('rollup.rollupIndexCapabilities', { indexPattern }), + ]); + + const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0; + const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0; + + const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields); + const timeFields = fieldCapabilitiesEntries.reduce((accumulatedTimeFields, [ fieldName, fieldCapability ]) => { + if (fieldCapability.date) { + accumulatedTimeFields.push(fieldName); + } + return accumulatedTimeFields; + }, []); + + reply({ + doesMatchIndices, + doesMatchRollupIndices, + timeFields, + }); + } catch(err) { + // 404s are still valid results. + if (err.statusCode === 404) { + return reply({ + doesMatchIndices: false, + doesMatchRollupIndices: false, + timeFields: [], + }); + } + + if (isEsError(err)) { + return reply(wrapEsError(err)); + } + + reply(wrapUnknownError(err)); + } + } + }); } diff --git a/x-pack/plugins/rollup/server/routes/api/jobs.js b/x-pack/plugins/rollup/server/routes/api/jobs.js index 59e2b0d9a7837..b911b4b68502f 100644 --- a/x-pack/plugins/rollup/server/routes/api/jobs.js +++ b/x-pack/plugins/rollup/server/routes/api/jobs.js @@ -5,14 +5,19 @@ */ import { callWithRequestFactory } from '../../lib/call_with_request_factory'; import { isEsErrorFactory } from '../../lib/is_es_error_factory'; +import { licensePreRoutingFactory } from'../../lib/license_pre_routing_factory'; import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; export function registerJobsRoute(server) { const isEsError = isEsErrorFactory(server); + const licensePreRouting = licensePreRoutingFactory(server); server.route({ path: '/api/rollup/jobs', method: 'GET', + config: { + pre: [ licensePreRouting ] + }, handler: async (request, reply) => { try { const callWithRequest = callWithRequestFactory(server, request); @@ -31,6 +36,9 @@ export function registerJobsRoute(server) { server.route({ path: '/api/rollup/create', method: 'PUT', + config: { + pre: [ licensePreRouting ] + }, handler: async (request, reply) => { try { const { @@ -62,6 +70,9 @@ export function registerJobsRoute(server) { server.route({ path: '/api/rollup/start', method: 'POST', + config: { + pre: [ licensePreRouting ] + }, handler: async (request, reply) => { try { const { jobIds } = request.payload; @@ -82,6 +93,9 @@ export function registerJobsRoute(server) { server.route({ path: '/api/rollup/stop', method: 'POST', + config: { + pre: [ licensePreRouting ] + }, handler: async (request, reply) => { try { const { jobIds } = request.payload; @@ -102,6 +116,9 @@ export function registerJobsRoute(server) { server.route({ path: '/api/rollup/delete', method: 'POST', + config: { + pre: [ licensePreRouting ] + }, handler: async (request, reply) => { try { const { jobIds } = request.payload; diff --git a/x-pack/plugins/rollup/server/routes/api/search.js b/x-pack/plugins/rollup/server/routes/api/search.js index ca7c8b4e63036..5718414d3cc99 100644 --- a/x-pack/plugins/rollup/server/routes/api/search.js +++ b/x-pack/plugins/rollup/server/routes/api/search.js @@ -5,14 +5,19 @@ */ import { callWithRequestFactory } from '../../lib/call_with_request_factory'; import { isEsErrorFactory } from '../../lib/is_es_error_factory'; +import { licensePreRoutingFactory } from'../../lib/license_pre_routing_factory'; import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; export function registerSearchRoute(server) { const isEsError = isEsErrorFactory(server); + const licensePreRouting = licensePreRoutingFactory(server); server.route({ path: '/api/rollup/search', method: 'POST', + config: { + pre: [ licensePreRouting ] + }, handler: async (request, reply) => { const callWithRequest = callWithRequestFactory(server, request);