Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enterprise Search] Migrate shared Schema components #84381

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*/

export const TEXT = 'text';
export const NUMBER = 'number';
export const DATE = 'date';
export const GEOLOCATION = 'geolocation';

export const fieldTypeSelectOptions = [
{ value: TEXT, text: TEXT },
{ value: NUMBER, text: NUMBER },
{ value: DATE, text: DATE },
{ value: GEOLOCATION, text: GEOLOCATION },
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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 { i18n } from '@kbn/i18n';

export const FIELD_NAME_CORRECT_NOTE = i18n.translate(
'xpack.enterpriseSearch.schema.addFieldModal.fieldNameNote.correct',
{
defaultMessage: 'Field names can only contain lowercase letters, numbers, and underscores',
}
);

export const FIELD_NAME_CORRECTED_PREFIX = i18n.translate(
'xpack.enterpriseSearch.schema.addFieldModal.fieldNameNote.corrected',
{
defaultMessage: 'The field will be named',
}
);

export const FIELD_NAME_MODAL_TITLE = i18n.translate(
'xpack.enterpriseSearch.schema.addFieldModal.fieldNameNote.title',
{
defaultMessage: 'Add a New Field',
}
);

export const FIELD_NAME_MODAL_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.schema.addFieldModal.fieldNameNote.description',
{
defaultMessage: 'Once added, a field cannot be removed from your schema.',
}
);

export const FIELD_NAME_MODAL_CANCEL = i18n.translate(
'xpack.enterpriseSearch.schema.addFieldModal.fieldNameNote.cancel',
{
defaultMessage: 'Cancel',
}
);

export const FIELD_NAME_MODAL_ADD_FIELD = i18n.translate(
'xpack.enterpriseSearch.schema.addFieldModal.fieldNameNote.addField',
{
defaultMessage: 'Add field',
}
);

export const ERROR_TABLE_ID_HEADER = i18n.translate(
'xpack.enterpriseSearch.schema.errorsTable.heading.id',
{
defaultMessage: 'id',
}
);

export const ERROR_TABLE_ERROR_HEADER = i18n.translate(
'xpack.enterpriseSearch.schema.errorsTable.heading.error',
{
defaultMessage: 'Error',
}
);

export const ERROR_TABLE_REVIEW_CONTROL = i18n.translate(
'xpack.enterpriseSearch.schema.errorsTable.control.review',
{
defaultMessage: 'Review',
}
);

export const ERROR_TABLE_VIEW_LINK = i18n.translate(
'xpack.enterpriseSearch.schema.errorsTable.link.view',
{
defaultMessage: 'View',
}
);

export const RECENTY_ADDED = i18n.translate(
'xpack.enterpriseSearch.schema.existingField.status.recentlyAdded',
{
defaultMessage: 'Recently Added',
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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 { SchemaAddFieldModal } from './schema_add_field_modal';
export { SchemaExistingField } from './schema_existing_field';
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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 { shallow, mount } from 'enzyme';

import { NUMBER } from '../constants/field_types';

import { FIELD_NAME_CORRECTED_PREFIX } from './constants';

import { SchemaAddFieldModal } from './';

import { EuiFieldText, EuiModal, EuiSelect } from '@elastic/eui';

describe('SchemaAddFieldModal', () => {
const addNewField = jest.fn();
const closeAddFieldModal = jest.fn();

const props = {
addNewField,
closeAddFieldModal,
};

const errors = {
addFieldFormErrors: ['error1', 'error2'],
};

const setState = jest.fn();
const setStateMock: any = (initState: any) => [initState, setState];

beforeEach(() => {
jest.spyOn(React, 'useState').mockImplementationOnce(setStateMock);
setState(false);
});

it('renders', () => {
const wrapper = shallow(<SchemaAddFieldModal {...props} />);
expect(wrapper.find(EuiModal)).toHaveLength(1);
});

// No matter what I try I can't get this to actually achieve coverage.
it('sets loading state in useEffect', () => {
setState(true);
const wrapper = mount(<SchemaAddFieldModal {...props} {...errors} />);
const input = wrapper.find(EuiFieldText);

expect(input.prop('isLoading')).toEqual(false);
expect(setState).toHaveBeenCalledTimes(3);
expect(setState).toHaveBeenCalledWith(false);
});

it('handles input change - with non-formatted name', () => {
jest.spyOn(React, 'useState').mockImplementationOnce(setStateMock);
const wrapper = shallow(<SchemaAddFieldModal {...props} />);
const input = wrapper.find(EuiFieldText);
input.simulate('change', { currentTarget: { value: 'foobar' } });

expect(wrapper.find('[data-test-subj="SchemaAddFieldNameRow"]').prop('helpText')).toEqual(
'Field names can only contain lowercase letters, numbers, and underscores'
);
});

it('handles input change - with formatted name', () => {
jest.spyOn(React, 'useState').mockImplementationOnce(setStateMock);
const wrapper = shallow(<SchemaAddFieldModal {...props} />);
const input = wrapper.find(EuiFieldText);
input.simulate('change', { currentTarget: { value: 'foo-bar' } });

expect(wrapper.find('[data-test-subj="SchemaAddFieldNameRow"]').prop('helpText')).toEqual(
<React.Fragment>
{FIELD_NAME_CORRECTED_PREFIX} <strong>foo_bar</strong>
</React.Fragment>
);
});

it('handles option change', () => {
const wrapper = shallow(<SchemaAddFieldModal {...props} />);
wrapper.find(EuiSelect).simulate('change', { target: { value: NUMBER } });

expect(wrapper.find('[data-test-subj="SchemaSelect"]').prop('value')).toEqual(NUMBER);
});

it('handles form submission', () => {
jest.spyOn(React, 'useState').mockImplementationOnce(setStateMock);
const wrapper = shallow(<SchemaAddFieldModal {...props} />);
const preventDefault = jest.fn();
wrapper.find('form').simulate('submit', { preventDefault });

expect(addNewField).toHaveBeenCalled();
expect(setState).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* 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, { ChangeEvent, FormEvent, useEffect, useState } from 'react';

import {
EuiButton,
EuiButtonEmpty,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiSelect,
EuiSpacer,
} from '@elastic/eui';

import { TEXT, fieldTypeSelectOptions } from '../constants/field_types';

import {
FIELD_NAME_CORRECT_NOTE,
FIELD_NAME_CORRECTED_PREFIX,
FIELD_NAME_MODAL_TITLE,
FIELD_NAME_MODAL_DESCRIPTION,
FIELD_NAME_MODAL_CANCEL,
FIELD_NAME_MODAL_ADD_FIELD,
} from './constants';

interface ISchemaAddFieldModalProps {
disableForm?: boolean;
addFieldFormErrors?: string[] | null;
addNewField(fieldName: string, newFieldType: string): void;
closeAddFieldModal(): void;
}

export const SchemaAddFieldModal: React.FC<ISchemaAddFieldModalProps> = ({
addNewField,
addFieldFormErrors,
closeAddFieldModal,
disableForm,
}) => {
const [loading, setLoading] = useState(false);
const [newFieldType, updateNewFieldType] = useState(TEXT);
const [formattedFieldName, setFormattedFieldName] = useState('');
const [rawFieldName, setRawFieldName] = useState('');

useEffect(() => {
if (addFieldFormErrors) setLoading(false);
}, [addFieldFormErrors]);

const handleChange = ({ currentTarget: { value } }: ChangeEvent<HTMLInputElement>) => {
setRawFieldName(value);
setFormattedFieldName(formatFieldName(value));
};

const submitForm = (e: FormEvent) => {
e.preventDefault();
addNewField(formattedFieldName, newFieldType);
setLoading(true);
};

const fieldNameNote =
rawFieldName !== formattedFieldName ? (
<>
{FIELD_NAME_CORRECTED_PREFIX} <strong>{formattedFieldName}</strong>
</>
) : (
FIELD_NAME_CORRECT_NOTE
);

return (
<EuiOverlayMask>
<form onSubmit={submitForm}>
<EuiModal onClose={closeAddFieldModal} maxWidth={500}>
<EuiModalHeader>
<EuiModalHeaderTitle>{FIELD_NAME_MODAL_TITLE}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<p>{FIELD_NAME_MODAL_DESCRIPTION}</p>
<EuiForm>
<EuiSpacer />
<EuiFlexGroup gutterSize="m">
<EuiFlexItem>
<EuiFormRow
label="Field name"
helpText={fieldNameNote}
fullWidth={true}
data-test-subj="SchemaAddFieldNameRow"
error={addFieldFormErrors}
isInvalid={!!addFieldFormErrors}
>
<EuiFieldText
placeholder="name"
type="text"
onChange={handleChange}
required={true}
value={rawFieldName}
fullWidth={true}
autoFocus={true}
isLoading={loading}
data-test-subj="SchemaAddFieldNameField"
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow label="Field type" data-test-subj="SchemaAddFieldTypeRow">
<EuiSelect
name="select-add"
required
value={newFieldType}
options={fieldTypeSelectOptions}
disabled={disableForm}
onChange={(e) => updateNewFieldType(e.target.value)}
data-test-subj="SchemaSelect"
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={closeAddFieldModal}>{FIELD_NAME_MODAL_CANCEL}</EuiButtonEmpty>
<EuiButton
color="primary"
fill={true}
disabled={disableForm}
type="submit"
isLoading={loading}
data-test-subj="SchemaAddFieldAddFieldButton"
>
{FIELD_NAME_MODAL_ADD_FIELD}
</EuiButton>
</EuiModalFooter>
</EuiModal>
</form>
</EuiOverlayMask>
);
};

const formatFieldName = (rawName: string) =>
rawName
.trim()
.replace(/[^a-zA-Z0-9]+/g, '_')
.replace(/^[^a-zA-Z0-9]+/, '')
.replace(/[^a-zA-Z0-9]+$/, '')
.toLowerCase();
Loading