[Alerting] change index action config executionTimeField to nullable (#61127)

resolves https://github.com/elastic/kibana/issues/61056

When the index action params moved into config, the `schema.maybe()` on the
`executionTimeField` should have been changed to `schema.nullable()`, otherwise
you can never "unset" the field, once it's set.

Changes rippled down to the UI as well.

To be extra safe, we also check that the `executionTimeField` isn't an empty
string when trimmed, as ES will not accept a document with a property that is
the empty string.
This commit is contained in:
Patrick Mueller 2020-03-24 23:19:56 -04:00 committed by GitHub
parent 683bf3a72e
commit aa73e2aee3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 60 additions and 15 deletions

View file

@ -52,6 +52,7 @@ describe('config validation', () => {
...config,
index: 'testing-123',
refresh: false,
executionTimeField: null,
});
config.executionTimeField = 'field-123';
@ -62,6 +63,14 @@ describe('config validation', () => {
executionTimeField: 'field-123',
});
config.executionTimeField = null;
expect(validateConfig(actionType, config)).toEqual({
...config,
index: 'testing-123',
refresh: false,
executionTimeField: null,
});
delete config.index;
expect(() => {
@ -73,9 +82,11 @@ describe('config validation', () => {
expect(() => {
validateConfig(actionType, { index: 'testing-123', executionTimeField: true });
}).toThrowErrorMatchingInlineSnapshot(
`"error validating action type config: [executionTimeField]: expected value of type [string] but got [boolean]"`
);
}).toThrowErrorMatchingInlineSnapshot(`
"error validating action type config: [executionTimeField]: types that failed validation:
- [executionTimeField.0]: expected value of type [string] but got [boolean]
- [executionTimeField.1]: expected value to equal [null]"
`);
delete config.refresh;
expect(() => {
@ -138,12 +149,12 @@ describe('params validation', () => {
describe('execute()', () => {
test('ensure parameters are as expected', async () => {
const secrets = {};
let config: ActionTypeConfigType;
let config: Partial<ActionTypeConfigType>;
let params: ActionParamsType;
let executorOptions: ActionTypeExecutorOptions;
// minimal params
config = { index: 'index-value', refresh: false, executionTimeField: undefined };
config = { index: 'index-value', refresh: false };
params = {
documents: [{ jim: 'bob' }],
};

View file

@ -18,7 +18,7 @@ export type ActionTypeConfigType = TypeOf<typeof ConfigSchema>;
const ConfigSchema = schema.object({
index: schema.string(),
refresh: schema.boolean({ defaultValue: false }),
executionTimeField: schema.maybe(schema.string()),
executionTimeField: schema.nullable(schema.string()),
});
// params definition
@ -63,8 +63,9 @@ async function executor(
const bulkBody = [];
for (const document of params.documents) {
if (config.executionTimeField != null) {
document[config.executionTimeField] = new Date();
const timeField = config.executionTimeField == null ? '' : config.executionTimeField.trim();
if (timeField !== '') {
document[timeField] = new Date();
}
bulkBody.push({ index: {} });

View file

@ -52,6 +52,26 @@ describe('index connector validation', () => {
});
});
describe('index connector validation with minimal config', () => {
test('connector validation succeeds when connector config is valid', () => {
const actionConnector = {
secrets: {},
id: 'test',
actionTypeId: '.index',
name: 'es_index',
config: {
index: 'test_es_index',
},
} as EsIndexActionConnector;
expect(actionTypeModel.validateConnector(actionConnector)).toEqual({
errors: {
index: [],
},
});
});
});
describe('action params validation', () => {
test('action params validation succeeds when action params is valid', () => {
const actionParams = {

View file

@ -79,7 +79,7 @@ const IndexActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsP
>> = ({ action, editActionConfig, errors, http }) => {
const { index, refresh, executionTimeField } = action.config;
const [hasTimeFieldCheckbox, setTimeFieldCheckboxState] = useState<boolean>(
executionTimeField !== undefined
executionTimeField != null
);
const [indexPatterns, setIndexPatterns] = useState([]);
@ -206,6 +206,11 @@ const IndexActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsP
checked={hasTimeFieldCheckbox || false}
onChange={() => {
setTimeFieldCheckboxState(!hasTimeFieldCheckbox);
// if changing from checked to not checked (hasTimeField === true),
// set time field to null
if (hasTimeFieldCheckbox) {
editActionConfig('executionTimeField', null);
}
}}
label={
<>
@ -245,13 +250,13 @@ const IndexActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsP
fullWidth
name="executionTimeField"
data-test-subj="executionTimeFieldSelect"
value={executionTimeField}
value={executionTimeField ?? ''}
onChange={e => {
editActionConfig('executionTimeField', e.target.value);
editActionConfig('executionTimeField', nullableString(e.target.value));
}}
onBlur={() => {
if (executionTimeField === undefined) {
editActionConfig('executionTimeField', '');
editActionConfig('executionTimeField', null);
}
}}
/>
@ -312,3 +317,9 @@ const IndexParamsFields: React.FunctionComponent<ActionParamsProps<IndexActionPa
</Fragment>
);
};
// if the string == null or is empty, return null, else return string
function nullableString(str: string | null | undefined) {
if (str == null || str.trim() === '') return null;
return str;
}

View file

@ -83,7 +83,7 @@ export interface EmailActionConnector extends ActionConnector {
interface EsIndexConfig {
index: string;
executionTimeField?: string;
executionTimeField?: string | null;
refresh?: boolean;
}

View file

@ -45,6 +45,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
config: {
index: ES_TEST_INDEX_NAME,
refresh: false,
executionTimeField: null,
},
});
createdActionID = createdAction.id;
@ -58,7 +59,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
id: fetchedAction.id,
name: 'An index action',
actionTypeId: '.index',
config: { index: ES_TEST_INDEX_NAME, refresh: false },
config: { index: ES_TEST_INDEX_NAME, refresh: false, executionTimeField: null },
});
// create action with all config props

View file

@ -43,6 +43,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
config: {
index: ES_TEST_INDEX_NAME,
refresh: false,
executionTimeField: null,
},
});
createdActionID = createdAction.id;
@ -56,7 +57,7 @@ export default function indexTest({ getService }: FtrProviderContext) {
id: fetchedAction.id,
name: 'An index action',
actionTypeId: '.index',
config: { index: ES_TEST_INDEX_NAME, refresh: false },
config: { index: ES_TEST_INDEX_NAME, refresh: false, executionTimeField: null },
});
// create action with all config props