Remove 'minute' frequency option from SLM policy form because ES won't allow a frequency faster than every 15 minutes. (#84854) (#85446)
* Add frequencyBlockList prop to CronEditor to allow selectively removing frequency options. * Add unit test coverage for CronEditor component. * Convert es_ui_shared cron editor component to TypeScript.
This commit is contained in:
parent
0af9f9c9bf
commit
f019d8e6b3
9764
src/plugins/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap
generated
Normal file
9764
src/plugins/es_ui_shared/public/components/cron_editor/__snapshots__/cron_editor.test.tsx.snap
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 { padStart } from 'lodash';
|
||||
import { EuiSelectOption } from '@elastic/eui';
|
||||
|
||||
import { DayOrdinal, MonthOrdinal, getOrdinalValue, getDayName, getMonthName } from './services';
|
||||
import { Frequency, Field, FieldToValueMap } from './types';
|
||||
|
||||
type FieldFlags = {
|
||||
[key in Field]?: boolean;
|
||||
};
|
||||
|
||||
function makeSequence(min: number, max: number): number[] {
|
||||
const values = [];
|
||||
for (let i = min; i <= max; i++) {
|
||||
values.push(i);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export const MINUTE_OPTIONS = makeSequence(0, 59).map((value) => ({
|
||||
value: value.toString(),
|
||||
text: padStart(value.toString(), 2, '0'),
|
||||
}));
|
||||
|
||||
export const HOUR_OPTIONS = makeSequence(0, 23).map((value) => ({
|
||||
value: value.toString(),
|
||||
text: padStart(value.toString(), 2, '0'),
|
||||
}));
|
||||
|
||||
export const DAY_OPTIONS = makeSequence(1, 7).map((value) => ({
|
||||
value: value.toString(),
|
||||
text: getDayName((value - 1) as DayOrdinal),
|
||||
}));
|
||||
|
||||
export const DATE_OPTIONS = makeSequence(1, 31).map((value) => ({
|
||||
value: value.toString(),
|
||||
text: getOrdinalValue(value),
|
||||
}));
|
||||
|
||||
export const MONTH_OPTIONS = makeSequence(1, 12).map((value) => ({
|
||||
value: value.toString(),
|
||||
text: getMonthName((value - 1) as MonthOrdinal),
|
||||
}));
|
||||
|
||||
export const UNITS: EuiSelectOption[] = [
|
||||
{
|
||||
value: 'MINUTE',
|
||||
text: 'minute',
|
||||
},
|
||||
{
|
||||
value: 'HOUR',
|
||||
text: 'hour',
|
||||
},
|
||||
{
|
||||
value: 'DAY',
|
||||
text: 'day',
|
||||
},
|
||||
{
|
||||
value: 'WEEK',
|
||||
text: 'week',
|
||||
},
|
||||
{
|
||||
value: 'MONTH',
|
||||
text: 'month',
|
||||
},
|
||||
{
|
||||
value: 'YEAR',
|
||||
text: 'year',
|
||||
},
|
||||
];
|
||||
|
||||
export const frequencyToFieldsMap: Record<Frequency, FieldFlags> = {
|
||||
MINUTE: {},
|
||||
HOUR: {
|
||||
minute: true,
|
||||
},
|
||||
DAY: {
|
||||
hour: true,
|
||||
minute: true,
|
||||
},
|
||||
WEEK: {
|
||||
day: true,
|
||||
hour: true,
|
||||
minute: true,
|
||||
},
|
||||
MONTH: {
|
||||
date: true,
|
||||
hour: true,
|
||||
minute: true,
|
||||
},
|
||||
YEAR: {
|
||||
month: true,
|
||||
date: true,
|
||||
hour: true,
|
||||
minute: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const frequencyToBaselineFieldsMap: Record<Frequency, FieldToValueMap> = {
|
||||
MINUTE: {
|
||||
second: '0',
|
||||
minute: '*',
|
||||
hour: '*',
|
||||
date: '*',
|
||||
month: '*',
|
||||
day: '?',
|
||||
},
|
||||
HOUR: {
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '*',
|
||||
date: '*',
|
||||
month: '*',
|
||||
day: '?',
|
||||
},
|
||||
DAY: {
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '0',
|
||||
date: '*',
|
||||
month: '*',
|
||||
day: '?',
|
||||
},
|
||||
WEEK: {
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '0',
|
||||
date: '?',
|
||||
month: '*',
|
||||
day: '7',
|
||||
},
|
||||
MONTH: {
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '0',
|
||||
date: '1',
|
||||
month: '*',
|
||||
day: '?',
|
||||
},
|
||||
YEAR: {
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '0',
|
||||
date: '1',
|
||||
month: '1',
|
||||
day: '?',
|
||||
},
|
||||
};
|
|
@ -18,13 +18,25 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
interface Props {
|
||||
minute?: string;
|
||||
minuteOptions: EuiSelectOption[];
|
||||
hour?: string;
|
||||
hourOptions: EuiSelectOption[];
|
||||
onChange: ({ minute, hour }: { minute?: string; hour?: string }) => void;
|
||||
}
|
||||
|
||||
export const CronDaily = ({ minute, minuteOptions, hour, hourOptions, onChange }) => (
|
||||
export const CronDaily: React.FunctionComponent<Props> = ({
|
||||
minute,
|
||||
minuteOptions,
|
||||
hour,
|
||||
hourOptions,
|
||||
onChange,
|
||||
}) => (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={
|
||||
|
@ -67,11 +79,3 @@ export const CronDaily = ({ minute, minuteOptions, hour, hourOptions, onChange }
|
|||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
CronDaily.propTypes = {
|
||||
minute: PropTypes.string.isRequired,
|
||||
minuteOptions: PropTypes.array.isRequired,
|
||||
hour: PropTypes.string.isRequired,
|
||||
hourOptions: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 sinon from 'sinon';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { mountWithI18nProvider } from '@kbn/test/jest';
|
||||
|
||||
import { Frequency } from './types';
|
||||
import { CronEditor } from './cron_editor';
|
||||
|
||||
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
|
||||
return {
|
||||
htmlIdGenerator: () => () => `generated-id`,
|
||||
};
|
||||
});
|
||||
|
||||
describe('CronEditor', () => {
|
||||
['MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'].forEach((unit) => {
|
||||
test(`is rendered with a ${unit} frequency`, () => {
|
||||
const component = mountWithI18nProvider(
|
||||
<CronEditor
|
||||
fieldToPreferredValueMap={{}}
|
||||
frequency={unit as Frequency}
|
||||
cronExpression="0 10 * * * ?"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('props', () => {
|
||||
describe('frequencyBlockList', () => {
|
||||
it('excludes the blocked frequencies from the frequency list', () => {
|
||||
const component = mountWithI18nProvider(
|
||||
<CronEditor
|
||||
frequencyBlockList={['HOUR', 'WEEK', 'YEAR']}
|
||||
fieldToPreferredValueMap={{}}
|
||||
frequency={'WEEK'}
|
||||
cronExpression="0 10 * * * ?"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const frequencySelect = findTestSubject(component, 'cronFrequencySelect');
|
||||
expect(frequencySelect.text()).toBe('minutedaymonth');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cronExpression', () => {
|
||||
it('sets the values of the fields', () => {
|
||||
const component = mountWithI18nProvider(
|
||||
<CronEditor
|
||||
fieldToPreferredValueMap={{}}
|
||||
frequency={'YEAR'}
|
||||
cronExpression="0 20 10 5 2 ?"
|
||||
onChange={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const monthSelect = findTestSubject(component, 'cronFrequencyYearlyMonthSelect');
|
||||
expect(monthSelect.props().value).toBe('2');
|
||||
|
||||
const dateSelect = findTestSubject(component, 'cronFrequencyYearlyDateSelect');
|
||||
expect(dateSelect.props().value).toBe('5');
|
||||
|
||||
const hourSelect = findTestSubject(component, 'cronFrequencyYearlyHourSelect');
|
||||
expect(hourSelect.props().value).toBe('10');
|
||||
|
||||
const minuteSelect = findTestSubject(component, 'cronFrequencyYearlyMinuteSelect');
|
||||
expect(minuteSelect.props().value).toBe('20');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onChange', () => {
|
||||
it('is called when the frequency changes', () => {
|
||||
const onChangeSpy = sinon.spy();
|
||||
const component = mountWithI18nProvider(
|
||||
<CronEditor
|
||||
fieldToPreferredValueMap={{}}
|
||||
frequency={'YEAR'}
|
||||
cronExpression="0 10 * * * ?"
|
||||
onChange={onChangeSpy}
|
||||
/>
|
||||
);
|
||||
|
||||
const frequencySelect = findTestSubject(component, 'cronFrequencySelect');
|
||||
frequencySelect.simulate('change', { target: { value: 'MONTH' } });
|
||||
|
||||
sinon.assert.calledWith(onChangeSpy, {
|
||||
cronExpression: '0 0 0 1 * ?',
|
||||
fieldToPreferredValueMap: {},
|
||||
frequency: 'MONTH',
|
||||
});
|
||||
});
|
||||
|
||||
it(`is called when a field's value changes`, () => {
|
||||
const onChangeSpy = sinon.spy();
|
||||
const component = mountWithI18nProvider(
|
||||
<CronEditor
|
||||
fieldToPreferredValueMap={{}}
|
||||
frequency={'YEAR'}
|
||||
cronExpression="0 10 * * * ?"
|
||||
onChange={onChangeSpy}
|
||||
/>
|
||||
);
|
||||
|
||||
const minuteSelect = findTestSubject(component, 'cronFrequencyYearlyMinuteSelect');
|
||||
minuteSelect.simulate('change', { target: { value: '40' } });
|
||||
|
||||
sinon.assert.calledWith(onChangeSpy, {
|
||||
cronExpression: '0 40 * * * ?',
|
||||
fieldToPreferredValueMap: { minute: '40' },
|
||||
frequency: 'YEAR',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -18,207 +18,86 @@
|
|||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { padStart } from 'lodash';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSelect, EuiFormRow, EuiSelectOption } from '@elastic/eui';
|
||||
|
||||
import { EuiSelect, EuiFormRow } from '@elastic/eui';
|
||||
import { Frequency, Field, FieldToValueMap } from './types';
|
||||
|
||||
import {
|
||||
getOrdinalValue,
|
||||
getDayName,
|
||||
getMonthName,
|
||||
cronExpressionToParts,
|
||||
cronPartsToExpression,
|
||||
MINUTE,
|
||||
HOUR,
|
||||
DAY,
|
||||
WEEK,
|
||||
MONTH,
|
||||
YEAR,
|
||||
} from './services';
|
||||
MINUTE_OPTIONS,
|
||||
HOUR_OPTIONS,
|
||||
DAY_OPTIONS,
|
||||
DATE_OPTIONS,
|
||||
MONTH_OPTIONS,
|
||||
UNITS,
|
||||
frequencyToFieldsMap,
|
||||
frequencyToBaselineFieldsMap,
|
||||
} from './constants';
|
||||
|
||||
import { cronExpressionToParts, cronPartsToExpression } from './services';
|
||||
import { CronHourly } from './cron_hourly';
|
||||
import { CronDaily } from './cron_daily';
|
||||
import { CronWeekly } from './cron_weekly';
|
||||
import { CronMonthly } from './cron_monthly';
|
||||
import { CronYearly } from './cron_yearly';
|
||||
|
||||
function makeSequence(min, max) {
|
||||
const values = [];
|
||||
for (let i = min; i <= max; i++) {
|
||||
values.push(i);
|
||||
const excludeBlockListedFrequencies = (
|
||||
units: EuiSelectOption[],
|
||||
blockListedUnits: string[] = []
|
||||
): EuiSelectOption[] => {
|
||||
if (blockListedUnits.length === 0) {
|
||||
return units;
|
||||
}
|
||||
return values;
|
||||
|
||||
return units.filter(({ value }) => !blockListedUnits.includes(value as string));
|
||||
};
|
||||
|
||||
interface Props {
|
||||
frequencyBlockList?: string[];
|
||||
fieldToPreferredValueMap: FieldToValueMap;
|
||||
frequency: Frequency;
|
||||
cronExpression: string;
|
||||
onChange: ({
|
||||
cronExpression,
|
||||
fieldToPreferredValueMap,
|
||||
frequency,
|
||||
}: {
|
||||
cronExpression: string;
|
||||
fieldToPreferredValueMap: FieldToValueMap;
|
||||
frequency: Frequency;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
const MINUTE_OPTIONS = makeSequence(0, 59).map((value) => ({
|
||||
value: value.toString(),
|
||||
text: padStart(value, 2, '0'),
|
||||
}));
|
||||
type State = FieldToValueMap;
|
||||
|
||||
const HOUR_OPTIONS = makeSequence(0, 23).map((value) => ({
|
||||
value: value.toString(),
|
||||
text: padStart(value, 2, '0'),
|
||||
}));
|
||||
|
||||
const DAY_OPTIONS = makeSequence(1, 7).map((value) => ({
|
||||
value: value.toString(),
|
||||
text: getDayName(value - 1),
|
||||
}));
|
||||
|
||||
const DATE_OPTIONS = makeSequence(1, 31).map((value) => ({
|
||||
value: value.toString(),
|
||||
text: getOrdinalValue(value),
|
||||
}));
|
||||
|
||||
const MONTH_OPTIONS = makeSequence(1, 12).map((value) => ({
|
||||
value: value.toString(),
|
||||
text: getMonthName(value - 1),
|
||||
}));
|
||||
|
||||
const UNITS = [
|
||||
{
|
||||
value: MINUTE,
|
||||
text: 'minute',
|
||||
},
|
||||
{
|
||||
value: HOUR,
|
||||
text: 'hour',
|
||||
},
|
||||
{
|
||||
value: DAY,
|
||||
text: 'day',
|
||||
},
|
||||
{
|
||||
value: WEEK,
|
||||
text: 'week',
|
||||
},
|
||||
{
|
||||
value: MONTH,
|
||||
text: 'month',
|
||||
},
|
||||
{
|
||||
value: YEAR,
|
||||
text: 'year',
|
||||
},
|
||||
];
|
||||
|
||||
const frequencyToFieldsMap = {
|
||||
[MINUTE]: {},
|
||||
[HOUR]: {
|
||||
minute: true,
|
||||
},
|
||||
[DAY]: {
|
||||
hour: true,
|
||||
minute: true,
|
||||
},
|
||||
[WEEK]: {
|
||||
day: true,
|
||||
hour: true,
|
||||
minute: true,
|
||||
},
|
||||
[MONTH]: {
|
||||
date: true,
|
||||
hour: true,
|
||||
minute: true,
|
||||
},
|
||||
[YEAR]: {
|
||||
month: true,
|
||||
date: true,
|
||||
hour: true,
|
||||
minute: true,
|
||||
},
|
||||
};
|
||||
|
||||
const frequencyToBaselineFieldsMap = {
|
||||
[MINUTE]: {
|
||||
second: '0',
|
||||
minute: '*',
|
||||
hour: '*',
|
||||
date: '*',
|
||||
month: '*',
|
||||
day: '?',
|
||||
},
|
||||
[HOUR]: {
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '*',
|
||||
date: '*',
|
||||
month: '*',
|
||||
day: '?',
|
||||
},
|
||||
[DAY]: {
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '0',
|
||||
date: '*',
|
||||
month: '*',
|
||||
day: '?',
|
||||
},
|
||||
[WEEK]: {
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '0',
|
||||
date: '?',
|
||||
month: '*',
|
||||
day: '7',
|
||||
},
|
||||
[MONTH]: {
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '0',
|
||||
date: '1',
|
||||
month: '*',
|
||||
day: '?',
|
||||
},
|
||||
[YEAR]: {
|
||||
second: '0',
|
||||
minute: '0',
|
||||
hour: '0',
|
||||
date: '1',
|
||||
month: '1',
|
||||
day: '?',
|
||||
},
|
||||
};
|
||||
|
||||
export class CronEditor extends Component {
|
||||
static propTypes = {
|
||||
fieldToPreferredValueMap: PropTypes.object.isRequired,
|
||||
frequency: PropTypes.string.isRequired,
|
||||
cronExpression: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props) {
|
||||
export class CronEditor extends Component<Props, State> {
|
||||
static getDerivedStateFromProps(props: Props) {
|
||||
const { cronExpression } = props;
|
||||
return cronExpressionToParts(cronExpression);
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const { cronExpression } = props;
|
||||
|
||||
const parsedCron = cronExpressionToParts(cronExpression);
|
||||
|
||||
this.state = {
|
||||
...parsedCron,
|
||||
};
|
||||
}
|
||||
|
||||
onChangeFrequency = (frequency) => {
|
||||
onChangeFrequency = (frequency: Frequency) => {
|
||||
const { onChange, fieldToPreferredValueMap } = this.props;
|
||||
|
||||
// Update fields which aren't editable with acceptable baseline values.
|
||||
const editableFields = Object.keys(frequencyToFieldsMap[frequency]);
|
||||
const inheritedFields = editableFields.reduce(
|
||||
(baselineFields, field) => {
|
||||
const editableFields = Object.keys(frequencyToFieldsMap[frequency]) as Field[];
|
||||
const inheritedFields = editableFields.reduce<FieldToValueMap>(
|
||||
(fieldBaselines, field) => {
|
||||
if (fieldToPreferredValueMap[field] != null) {
|
||||
baselineFields[field] = fieldToPreferredValueMap[field];
|
||||
fieldBaselines[field] = fieldToPreferredValueMap[field];
|
||||
}
|
||||
return baselineFields;
|
||||
return fieldBaselines;
|
||||
},
|
||||
{ ...frequencyToBaselineFieldsMap[frequency] }
|
||||
);
|
||||
|
@ -232,18 +111,21 @@ export class CronEditor extends Component {
|
|||
});
|
||||
};
|
||||
|
||||
onChangeFields = (fields) => {
|
||||
onChangeFields = (fields: FieldToValueMap) => {
|
||||
const { onChange, frequency, fieldToPreferredValueMap } = this.props;
|
||||
|
||||
const editableFields = Object.keys(frequencyToFieldsMap[frequency]);
|
||||
const newFieldToPreferredValueMap = {};
|
||||
const editableFields = Object.keys(frequencyToFieldsMap[frequency]) as Field[];
|
||||
const newFieldToPreferredValueMap: FieldToValueMap = {};
|
||||
|
||||
const editedFields = editableFields.reduce(
|
||||
const editedFields = editableFields.reduce<FieldToValueMap>(
|
||||
(accumFields, field) => {
|
||||
if (fields[field] !== undefined) {
|
||||
accumFields[field] = fields[field];
|
||||
// Once the user touches a field, we want to persist its value as the user changes
|
||||
// the cron frequency.
|
||||
// If the user changes a field's value, we want to maintain that value in the relevant
|
||||
// field, even as the frequency field changes. For example, if the user selects "Monthly"
|
||||
// frequency and changes the "Hour" field to "10", that field should still say "10" if the
|
||||
// user changes the frequency to "Weekly". We'll support this UX by storing these values
|
||||
// in the fieldToPreferredValueMap.
|
||||
newFieldToPreferredValueMap[field] = fields[field];
|
||||
} else {
|
||||
accumFields[field] = this.state[field];
|
||||
|
@ -271,10 +153,10 @@ export class CronEditor extends Component {
|
|||
const { minute, hour, day, date, month } = this.state;
|
||||
|
||||
switch (frequency) {
|
||||
case MINUTE:
|
||||
case 'MINUTE':
|
||||
return;
|
||||
|
||||
case HOUR:
|
||||
case 'HOUR':
|
||||
return (
|
||||
<CronHourly
|
||||
minute={minute}
|
||||
|
@ -283,7 +165,7 @@ export class CronEditor extends Component {
|
|||
/>
|
||||
);
|
||||
|
||||
case DAY:
|
||||
case 'DAY':
|
||||
return (
|
||||
<CronDaily
|
||||
minute={minute}
|
||||
|
@ -294,7 +176,7 @@ export class CronEditor extends Component {
|
|||
/>
|
||||
);
|
||||
|
||||
case WEEK:
|
||||
case 'WEEK':
|
||||
return (
|
||||
<CronWeekly
|
||||
minute={minute}
|
||||
|
@ -307,7 +189,7 @@ export class CronEditor extends Component {
|
|||
/>
|
||||
);
|
||||
|
||||
case MONTH:
|
||||
case 'MONTH':
|
||||
return (
|
||||
<CronMonthly
|
||||
minute={minute}
|
||||
|
@ -320,7 +202,7 @@ export class CronEditor extends Component {
|
|||
/>
|
||||
);
|
||||
|
||||
case YEAR:
|
||||
case 'YEAR':
|
||||
return (
|
||||
<CronYearly
|
||||
minute={minute}
|
||||
|
@ -341,7 +223,7 @@ export class CronEditor extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { frequency } = this.props;
|
||||
const { frequency, frequencyBlockList } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
@ -352,9 +234,11 @@ export class CronEditor extends Component {
|
|||
fullWidth
|
||||
>
|
||||
<EuiSelect
|
||||
options={UNITS}
|
||||
options={excludeBlockListedFrequencies(UNITS, frequencyBlockList)}
|
||||
value={frequency}
|
||||
onChange={(e) => this.onChangeFrequency(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
this.onChangeFrequency(e.target.value as Frequency)
|
||||
}
|
||||
fullWidth
|
||||
prepend={i18n.translate('esUi.cronEditor.textEveryLabel', {
|
||||
defaultMessage: 'Every',
|
|
@ -18,13 +18,17 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui';
|
||||
|
||||
import { EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
interface Props {
|
||||
minute?: string;
|
||||
minuteOptions: EuiSelectOption[];
|
||||
onChange: ({ minute }: { minute?: string }) => void;
|
||||
}
|
||||
|
||||
export const CronHourly = ({ minute, minuteOptions, onChange }) => (
|
||||
export const CronHourly: React.FunctionComponent<Props> = ({ minute, minuteOptions, onChange }) => (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={
|
||||
|
@ -46,9 +50,3 @@ export const CronHourly = ({ minute, minuteOptions, onChange }) => (
|
|||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
CronHourly.propTypes = {
|
||||
minute: PropTypes.string.isRequired,
|
||||
minuteOptions: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -18,13 +18,21 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
interface Props {
|
||||
minute?: string;
|
||||
minuteOptions: EuiSelectOption[];
|
||||
hour?: string;
|
||||
hourOptions: EuiSelectOption[];
|
||||
date?: string;
|
||||
dateOptions: EuiSelectOption[];
|
||||
onChange: ({ minute, hour, date }: { minute?: string; hour?: string; date?: string }) => void;
|
||||
}
|
||||
|
||||
export const CronMonthly = ({
|
||||
export const CronMonthly: React.FunctionComponent<Props> = ({
|
||||
minute,
|
||||
minuteOptions,
|
||||
hour,
|
||||
|
@ -94,13 +102,3 @@ export const CronMonthly = ({
|
|||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
CronMonthly.propTypes = {
|
||||
minute: PropTypes.string.isRequired,
|
||||
minuteOptions: PropTypes.array.isRequired,
|
||||
hour: PropTypes.string.isRequired,
|
||||
hourOptions: PropTypes.array.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
dateOptions: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -18,13 +18,21 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
interface Props {
|
||||
minute?: string;
|
||||
minuteOptions: EuiSelectOption[];
|
||||
hour?: string;
|
||||
hourOptions: EuiSelectOption[];
|
||||
day?: string;
|
||||
dayOptions: EuiSelectOption[];
|
||||
onChange: ({ minute, hour, day }: { minute?: string; hour?: string; day?: string }) => void;
|
||||
}
|
||||
|
||||
export const CronWeekly = ({
|
||||
export const CronWeekly: React.FunctionComponent<Props> = ({
|
||||
minute,
|
||||
minuteOptions,
|
||||
hour,
|
||||
|
@ -94,13 +102,3 @@ export const CronWeekly = ({
|
|||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
CronWeekly.propTypes = {
|
||||
minute: PropTypes.string.isRequired,
|
||||
minuteOptions: PropTypes.array.isRequired,
|
||||
hour: PropTypes.string.isRequired,
|
||||
hourOptions: PropTypes.array.isRequired,
|
||||
day: PropTypes.string.isRequired,
|
||||
dayOptions: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -18,13 +18,34 @@
|
|||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui';
|
||||
|
||||
export const CronYearly = ({
|
||||
interface Props {
|
||||
minute?: string;
|
||||
minuteOptions: EuiSelectOption[];
|
||||
hour?: string;
|
||||
hourOptions: EuiSelectOption[];
|
||||
date?: string;
|
||||
dateOptions: EuiSelectOption[];
|
||||
month?: string;
|
||||
monthOptions: EuiSelectOption[];
|
||||
onChange: ({
|
||||
minute,
|
||||
hour,
|
||||
date,
|
||||
month,
|
||||
}: {
|
||||
minute?: string;
|
||||
hour?: string;
|
||||
date?: string;
|
||||
month?: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const CronYearly: React.FunctionComponent<Props> = ({
|
||||
minute,
|
||||
minuteOptions,
|
||||
hour,
|
||||
|
@ -115,15 +136,3 @@ export const CronYearly = ({
|
|||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
CronYearly.propTypes = {
|
||||
minute: PropTypes.string.isRequired,
|
||||
minuteOptions: PropTypes.array.isRequired,
|
||||
hour: PropTypes.string.isRequired,
|
||||
hourOptions: PropTypes.array.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
dateOptions: PropTypes.array.isRequired,
|
||||
month: PropTypes.string.isRequired,
|
||||
monthOptions: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -17,5 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { Frequency } from './types';
|
||||
export { CronEditor } from './cron_editor';
|
||||
export { MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from './services';
|
|
@ -17,15 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export const MINUTE = 'MINUTE';
|
||||
export const HOUR = 'HOUR';
|
||||
export const DAY = 'DAY';
|
||||
export const WEEK = 'WEEK';
|
||||
export const MONTH = 'MONTH';
|
||||
export const YEAR = 'YEAR';
|
||||
import { FieldToValueMap } from '../types';
|
||||
|
||||
export function cronExpressionToParts(expression) {
|
||||
const parsedCron = {
|
||||
export function cronExpressionToParts(expression: string): FieldToValueMap {
|
||||
const parsedCron: FieldToValueMap = {
|
||||
second: undefined,
|
||||
minute: undefined,
|
||||
hour: undefined,
|
||||
|
@ -63,6 +58,13 @@ export function cronExpressionToParts(expression) {
|
|||
return parsedCron;
|
||||
}
|
||||
|
||||
export function cronPartsToExpression({ second, minute, hour, day, date, month }) {
|
||||
export function cronPartsToExpression({
|
||||
second,
|
||||
minute,
|
||||
hour,
|
||||
day,
|
||||
date,
|
||||
month,
|
||||
}: FieldToValueMap): string {
|
||||
return `${second} ${minute} ${hour} ${date} ${month} ${day}`;
|
||||
}
|
|
@ -19,6 +19,9 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export type DayOrdinal = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
export type MonthOrdinal = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
|
||||
|
||||
// The international ISO standard dictates Monday as the first day of the week, but cron patterns
|
||||
// use Sunday as the first day, so we're going with the cron way.
|
||||
const dayOrdinalToDayNameMap = {
|
||||
|
@ -46,7 +49,7 @@ const monthOrdinalToMonthNameMap = {
|
|||
11: i18n.translate('esUi.cronEditor.month.december', { defaultMessage: 'December' }),
|
||||
};
|
||||
|
||||
export function getOrdinalValue(number) {
|
||||
export function getOrdinalValue(number: number): string {
|
||||
// TODO: This is breaking reporting pdf generation. Possibly due to phantom not setting locale,
|
||||
// which is needed by i18n (formatjs). Need to verify, fix, and restore i18n in place of static stings.
|
||||
// return i18n.translate('esUi.cronEditor.number.ordinal', {
|
||||
|
@ -57,15 +60,16 @@ export function getOrdinalValue(number) {
|
|||
|
||||
// Protects against falsey (including 0) values
|
||||
const num = number && number.toString();
|
||||
let lastDigit = num && num.substr(-1);
|
||||
const lastDigitString = num && num.substr(-1);
|
||||
let ordinal;
|
||||
|
||||
if (!lastDigit) {
|
||||
return number;
|
||||
if (!lastDigitString) {
|
||||
return number.toString();
|
||||
}
|
||||
lastDigit = parseFloat(lastDigit);
|
||||
|
||||
switch (lastDigit) {
|
||||
const lastDigitNumeric = parseFloat(lastDigitString);
|
||||
|
||||
switch (lastDigitNumeric) {
|
||||
case 1:
|
||||
ordinal = 'st';
|
||||
break;
|
||||
|
@ -82,10 +86,10 @@ export function getOrdinalValue(number) {
|
|||
return `${num}${ordinal}`;
|
||||
}
|
||||
|
||||
export function getDayName(dayOrdinal) {
|
||||
export function getDayName(dayOrdinal: DayOrdinal): string {
|
||||
return dayOrdinalToDayNameMap[dayOrdinal];
|
||||
}
|
||||
|
||||
export function getMonthName(monthOrdinal) {
|
||||
export function getMonthName(monthOrdinal: MonthOrdinal): string {
|
||||
return monthOrdinalToMonthNameMap[monthOrdinal];
|
||||
}
|
|
@ -17,5 +17,11 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './cron';
|
||||
export * from './humanized_numbers';
|
||||
export { cronExpressionToParts, cronPartsToExpression } from './cron';
|
||||
export {
|
||||
getOrdinalValue,
|
||||
getDayName,
|
||||
getMonthName,
|
||||
DayOrdinal,
|
||||
MonthOrdinal,
|
||||
} from './humanized_numbers';
|
|
@ -17,10 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export declare const MINUTE: string;
|
||||
export declare const HOUR: string;
|
||||
export declare const DAY: string;
|
||||
export declare const WEEK: string;
|
||||
export declare const MONTH: string;
|
||||
export declare const YEAR: string;
|
||||
export declare const CronEditor: any;
|
||||
export type Frequency = 'MINUTE' | 'HOUR' | 'DAY' | 'WEEK' | 'MONTH' | 'YEAR';
|
||||
export type Field = 'second' | 'minute' | 'hour' | 'day' | 'date' | 'month';
|
||||
export type FieldToValueMap = {
|
||||
[key in Field]?: string;
|
||||
};
|
|
@ -30,7 +30,7 @@ export { JsonEditor, OnJsonEditorUpdateHandler, JsonEditorState } from './compon
|
|||
|
||||
export { SectionLoading } from './components/section_loading';
|
||||
|
||||
export { CronEditor, MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from './components/cron_editor';
|
||||
export { Frequency, CronEditor } from './components/cron_editor';
|
||||
|
||||
export {
|
||||
SendRequestConfig,
|
||||
|
|
|
@ -4,14 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
MINUTE,
|
||||
HOUR,
|
||||
DAY,
|
||||
WEEK,
|
||||
MONTH,
|
||||
YEAR,
|
||||
} from '../../../../../../src/plugins/es_ui_shared/public';
|
||||
import { indexPatterns } from '../../../../../../src/plugins/data/public';
|
||||
import { setHttp } from '../../crud_app/services';
|
||||
import { mockHttpRequest, pageHelpers } from './helpers';
|
||||
|
@ -190,14 +182,14 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
|
||||
describe('every minute', () => {
|
||||
it('should not have any additional configuration', () => {
|
||||
changeFrequency(MINUTE);
|
||||
changeFrequency('MINUTE');
|
||||
expect(find('cronFrequencyConfiguration').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hourly', () => {
|
||||
beforeEach(() => {
|
||||
changeFrequency(HOUR);
|
||||
changeFrequency('HOUR');
|
||||
});
|
||||
|
||||
it('should have 1 additional configuration', () => {
|
||||
|
@ -214,7 +206,7 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
|
||||
describe('daily', () => {
|
||||
beforeEach(() => {
|
||||
changeFrequency(DAY);
|
||||
changeFrequency('DAY');
|
||||
});
|
||||
|
||||
it('should have 1 additional configuration with hour and minute selects', () => {
|
||||
|
@ -238,7 +230,7 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
|
||||
describe('weekly', () => {
|
||||
beforeEach(() => {
|
||||
changeFrequency(WEEK);
|
||||
changeFrequency('WEEK');
|
||||
});
|
||||
|
||||
it('should have 2 additional configurations with day, hour and minute selects', () => {
|
||||
|
@ -277,7 +269,7 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
|
||||
describe('monthly', () => {
|
||||
beforeEach(() => {
|
||||
changeFrequency(MONTH);
|
||||
changeFrequency('MONTH');
|
||||
});
|
||||
|
||||
it('should have 2 additional configurations with date, hour and minute selects', () => {
|
||||
|
@ -308,7 +300,7 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
|
||||
describe('yearly', () => {
|
||||
beforeEach(() => {
|
||||
changeFrequency(YEAR);
|
||||
changeFrequency('YEAR');
|
||||
});
|
||||
|
||||
it('should have 3 additional configurations with month, date, hour and minute selects', () => {
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { Repository } from '../../../../../common/types';
|
||||
import { CronEditor, SectionError } from '../../../../shared_imports';
|
||||
import { Frequency, CronEditor, SectionError } from '../../../../shared_imports';
|
||||
import { useServices } from '../../../app_context';
|
||||
import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants';
|
||||
import { useLoadRepositories } from '../../../services/http';
|
||||
|
@ -71,7 +71,7 @@ export const PolicyStepLogistics: React.FunctionComponent<StepProps> = ({
|
|||
// State for cron editor
|
||||
const [simpleCron, setSimpleCron] = useState<{
|
||||
expression: string;
|
||||
frequency: string;
|
||||
frequency: Frequency;
|
||||
}>({
|
||||
expression: DEFAULT_POLICY_SCHEDULE,
|
||||
frequency: DEFAULT_POLICY_FREQUENCY,
|
||||
|
@ -480,6 +480,7 @@ export const PolicyStepLogistics: React.FunctionComponent<StepProps> = ({
|
|||
) : (
|
||||
<Fragment>
|
||||
<CronEditor
|
||||
frequencyBlockList={['MINUTE']} // ES disallows a frequency faster than 15m
|
||||
fieldToPreferredValueMap={fieldToPreferredValueMap}
|
||||
cronExpression={simpleCron.expression}
|
||||
frequency={simpleCron.frequency}
|
||||
|
@ -487,10 +488,6 @@ export const PolicyStepLogistics: React.FunctionComponent<StepProps> = ({
|
|||
cronExpression: expression,
|
||||
frequency,
|
||||
fieldToPreferredValueMap: newFieldToPreferredValueMap,
|
||||
}: {
|
||||
cronExpression: string;
|
||||
frequency: string;
|
||||
fieldToPreferredValueMap: any;
|
||||
}) => {
|
||||
setSimpleCron({
|
||||
expression,
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
|
||||
import { useServices, useToastNotifications } from '../app_context';
|
||||
import { documentationLinksService } from '../services/documentation';
|
||||
import { CronEditor } from '../../shared_imports';
|
||||
import { Frequency, CronEditor } from '../../shared_imports';
|
||||
import { DEFAULT_RETENTION_SCHEDULE, DEFAULT_RETENTION_FREQUENCY } from '../constants';
|
||||
import { updateRetentionSchedule } from '../services/http';
|
||||
|
||||
|
@ -57,7 +57,7 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent<Props
|
|||
|
||||
const [simpleCron, setSimpleCron] = useState<{
|
||||
expression: string;
|
||||
frequency: string;
|
||||
frequency: Frequency;
|
||||
}>({
|
||||
expression: DEFAULT_RETENTION_SCHEDULE,
|
||||
frequency: DEFAULT_RETENTION_FREQUENCY,
|
||||
|
@ -234,10 +234,6 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent<Props
|
|||
cronExpression: expression,
|
||||
frequency,
|
||||
fieldToPreferredValueMap: newFieldToPreferredValueMap,
|
||||
}: {
|
||||
cronExpression: string;
|
||||
frequency: string;
|
||||
fieldToPreferredValueMap: any;
|
||||
}) => {
|
||||
setSimpleCron({
|
||||
expression,
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { DAY } from '../../shared_imports';
|
||||
|
||||
export const BASE_PATH = '';
|
||||
export const DEFAULT_SECTION: Section = 'snapshots';
|
||||
export type Section = 'repositories' | 'snapshots' | 'restore_status' | 'policies';
|
||||
|
@ -89,10 +87,10 @@ export const REMOVE_INDEX_SETTINGS_SUGGESTIONS: string[] = INDEX_SETTING_SUGGEST
|
|||
);
|
||||
|
||||
export const DEFAULT_POLICY_SCHEDULE = '0 30 1 * * ?';
|
||||
export const DEFAULT_POLICY_FREQUENCY = DAY;
|
||||
export const DEFAULT_POLICY_FREQUENCY = 'DAY';
|
||||
|
||||
export const DEFAULT_RETENTION_SCHEDULE = '0 30 1 * * ?';
|
||||
export const DEFAULT_RETENTION_FREQUENCY = DAY;
|
||||
export const DEFAULT_RETENTION_FREQUENCY = 'DAY';
|
||||
|
||||
// UI Metric constants
|
||||
export const UIM_APP_NAME = 'snapshot_restore';
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
export {
|
||||
AuthorizationProvider,
|
||||
CronEditor,
|
||||
DAY,
|
||||
Error,
|
||||
Frequency,
|
||||
NotAuthorizedSection,
|
||||
SectionError,
|
||||
sendRequest,
|
||||
|
|
Loading…
Reference in a new issue