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

Index pattern field editor - Add warning and type 'confirm' on delete or save #95237

Merged
merged 12 commits into from
Mar 26, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,49 @@
* Side Public License, v 1.
*/

import React from 'react';
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiConfirmModal } from '@elastic/eui';
import { EuiCallOut, EuiConfirmModal, EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui';

const geti18nTexts = (fieldsToDelete?: string[]) => {
let modalTitle = '';
let confirmButtonText = '';
if (fieldsToDelete) {
const isSingle = fieldsToDelete.length === 1;

modalTitle = isSingle
? i18n.translate(
'indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteSingleTitle',
{
defaultMessage: `Remove field '{name}'?`,
defaultMessage: `Remove field '{name}'`,
values: { name: fieldsToDelete[0] },
}
)
: i18n.translate(
'indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteMultipleTitle',
{
defaultMessage: `Remove {count} fields?`,
defaultMessage: `Remove {count} fields`,
values: { count: fieldsToDelete.length },
}
);
confirmButtonText = isSingle
? i18n.translate(
'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.removeButtonLabel',
{
defaultMessage: `Remove field`,
}
)
: i18n.translate(
'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.removeMultipleButtonLabel',
{
defaultMessage: `Remove fields`,
}
);
}

return {
modalTitle,
confirmButtonText: i18n.translate(
'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.removeButtonLabel',
{
defaultMessage: 'Remove',
}
),
confirmButtonText,
cancelButtonText: i18n.translate(
'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.cancelButtonLabel',
{
Expand All @@ -52,6 +61,19 @@ const geti18nTexts = (fieldsToDelete?: string[]) => {
defaultMessage: 'You are about to remove these runtime fields:',
}
),
typeConfirm: i18n.translate(
'indexPatternFieldEditor.deleteRuntimeField.confirmModal.typeConfirm',
{
defaultMessage: "Type 'REMOVE' to confirm",
}
),
warningRemovingFields: i18n.translate(
'indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningRemovingFields',
{
defaultMessage:
'Warning: Removing fields may break searches or visualizations that rely on this field.',
}
),
};
};

Expand All @@ -65,6 +87,7 @@ export function DeleteFieldModal({ fieldsToDelete, closeModal, confirmDelete }:
const i18nTexts = geti18nTexts(fieldsToDelete);
const { modalTitle, confirmButtonText, cancelButtonText, warningMultipleFields } = i18nTexts;
const isMultiple = Boolean(fieldsToDelete.length > 1);
const [confirmContent, setConfirmContent] = useState<string>();
return (
<EuiConfirmModal
title={modalTitle}
Expand All @@ -74,17 +97,28 @@ export function DeleteFieldModal({ fieldsToDelete, closeModal, confirmDelete }:
cancelButtonText={cancelButtonText}
buttonColor="danger"
confirmButtonText={confirmButtonText}
confirmButtonDisabled={confirmContent?.toUpperCase() !== 'REMOVE'}
>
{isMultiple && (
<>
<p>{warningMultipleFields}</p>
<ul>
{fieldsToDelete.map((fieldName) => (
<li key={fieldName}>{fieldName}</li>
))}
</ul>
</>
)}
<EuiCallOut color="warning" title={i18nTexts.warningRemovingFields} iconType="alert" size="s">
{isMultiple && (
<>
<p>{warningMultipleFields}</p>
<ul>
{fieldsToDelete.map((fieldName) => (
<li key={fieldName}>{fieldName}</li>
))}
</ul>
</>
)}
</EuiCallOut>
<EuiSpacer />
<EuiFormRow label={i18nTexts.typeConfirm}>
<EuiFieldText
value={confirmContent}
onChange={(e) => setConfirmContent(e.target.value)}
data-test-subj="deleteModalConfirmText"
/>
</EuiFormRow>
</EuiConfirmModal>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('<FieldEditorFlyoutContent />', () => {

const { find } = setup({ ...defaultProps, field });

expect(find('flyoutTitle').text()).toBe(`Edit ${field.name} field`);
expect(find('flyoutTitle').text()).toBe(`Edit field 'foo'`);
expect(find('nameField.input').props().value).toBe(field.name);
expect(find('typeField').props().value).toBe(field.type);
expect(find('scriptField').props().value).toBe(field.script.source);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import React, { useState, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlyoutHeader,
EuiFlyoutBody,
Expand All @@ -19,6 +20,10 @@ import {
EuiButton,
EuiCallOut,
EuiSpacer,
EuiText,
EuiConfirmModal,
EuiFieldText,
EuiFormRow,
} from '@elastic/eui';

import { DocLinksStart, CoreStart } from 'src/core/public';
Expand All @@ -30,16 +35,6 @@ import type { Props as FieldEditorProps, FieldEditorFormState } from './field_ed

const geti18nTexts = (field?: Field) => {
return {
flyoutTitle: field
? i18n.translate('indexPatternFieldEditor.editor.flyoutEditFieldTitle', {
defaultMessage: 'Edit {fieldName} field',
values: {
fieldName: field.name,
},
})
: i18n.translate('indexPatternFieldEditor.editor.flyoutDefaultTitle', {
defaultMessage: 'Create field',
}),
closeButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutCloseButtonLabel', {
defaultMessage: 'Close',
}),
Expand All @@ -49,6 +44,31 @@ const geti18nTexts = (field?: Field) => {
formErrorsCalloutTitle: i18n.translate('indexPatternFieldEditor.editor.validationErrorTitle', {
defaultMessage: 'Fix errors in form before continuing.',
}),
cancelButtonText: i18n.translate(
'indexPatternFieldEditor.saveRuntimeField.confirmationModal.cancelButtonLabel',
{
defaultMessage: 'Cancel',
}
),
confirmButtonText: i18n.translate(
'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.saveButtonLabel',
{
defaultMessage: 'Save',
}
),
warningChangingFields: i18n.translate(
'indexPatternFieldEditor.deleteRuntimeField.confirmModal.warningChangingFields',
{
defaultMessage:
'Warning: Changing name or type may break searches or visualizations that rely on this field.',
}
),
typeConfirm: i18n.translate(
'indexPatternFieldEditor.saveRuntimeField.confirmModal.typeConfirm',
{
defaultMessage: "Type 'CHANGE' to continue:",
}
),
};
};

Expand Down Expand Up @@ -97,6 +117,7 @@ const FieldEditorFlyoutContentComponent = ({
runtimeFieldValidator,
isSavingField,
}: Props) => {
const isEditingExistingField = !!field;
const i18nTexts = geti18nTexts(field);

const [formState, setFormState] = useState<FieldEditorFormState>({
Expand All @@ -112,6 +133,8 @@ const FieldEditorFlyoutContentComponent = ({
);

const [isValidating, setIsValidating] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const [confirmContent, setConfirmContent] = useState<string>();

const { submit, isValid: isFormValid, isSubmitted } = formState;
const { fields } = indexPattern;
Expand All @@ -129,6 +152,8 @@ const FieldEditorFlyoutContentComponent = ({

const onClickSave = useCallback(async () => {
const { isValid, data } = await submit();
const nameChange = field?.name !== data.name;
const typeChange = field?.type !== data.type;

if (isValid) {
if (data.script) {
Expand All @@ -147,9 +172,13 @@ const FieldEditorFlyoutContentComponent = ({
}
}

onSave(data);
if (isEditingExistingField && (nameChange || typeChange)) {
setIsModalVisible(true);
} else {
onSave(data);
}
}
}, [onSave, submit, runtimeFieldValidator]);
}, [onSave, submit, runtimeFieldValidator, field, isEditingExistingField]);

const namesNotAllowed = useMemo(() => fields.map((fld) => fld.name), [fields]);

Expand Down Expand Up @@ -180,12 +209,70 @@ const FieldEditorFlyoutContentComponent = ({
[fieldTypeToProcess, namesNotAllowed, existingConcreteFields]
);

const modal = isModalVisible ? (
<EuiConfirmModal
title={`Confirm changes to '${field?.name}'`}
data-test-subj="runtimeFieldSaveConfirmModal"
cancelButtonText={i18nTexts.cancelButtonText}
confirmButtonText={i18nTexts.confirmButtonText}
confirmButtonDisabled={confirmContent?.toUpperCase() !== 'CHANGE'}
onCancel={() => {
setIsModalVisible(false);
setConfirmContent('');
}}
onConfirm={async () => {
const { data } = await submit();
onSave(data);
}}
>
<EuiCallOut
color="warning"
title={i18nTexts.warningChangingFields}
iconType="alert"
size="s"
/>
<EuiSpacer />
<EuiFormRow label={i18nTexts.typeConfirm}>
<EuiFieldText
value={confirmContent}
onChange={(e) => setConfirmContent(e.target.value)}
data-test-subj="saveModalConfirmText"
/>
</EuiFormRow>
</EuiConfirmModal>
) : null;
return (
<>
<EuiFlyoutHeader>
<EuiTitle size="m" data-test-subj="flyoutTitle">
<h2 id="fieldEditorTitle">{i18nTexts.flyoutTitle}</h2>
<EuiTitle data-test-subj="flyoutTitle">
<h2>
{field ? (
<FormattedMessage
id="indexPatternFieldEditor.editor.flyoutEditFieldTitle"
defaultMessage="Edit field '{fieldName}'"
values={{
fieldName: field.name,
}}
/>
) : (
<FormattedMessage
id="indexPatternFieldEditor.editor.flyoutDefaultTitle"
defaultMessage="Create field"
/>
)}
</h2>
</EuiTitle>
<EuiText color="subdued">
<p>
<FormattedMessage
id="indexPatternFieldEditor.editor.flyoutEditFieldSubtitle"
defaultMessage="Index pattern: {patternName}"
values={{
patternName: <i>{indexPattern.title}</i>,
}}
/>
</p>
</EuiText>
</EuiFlyoutHeader>

<EuiFlyoutBody>
Expand Down Expand Up @@ -246,6 +333,7 @@ const FieldEditorFlyoutContentComponent = ({
</>
)}
</EuiFlyoutFooter>
{modal}
</>
);
};
Expand Down
15 changes: 15 additions & 0 deletions test/functional/apps/management/_runtime_fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function ({ getService, getPageObjects }) {
const browser = getService('browser');
const retry = getService('retry');
const PageObjects = getPageObjects(['settings']);
const testSubjects = getService('testSubjects');

describe('runtime fields', function () {
this.tags(['skipFirefox']);
Expand Down Expand Up @@ -47,6 +48,20 @@ export default function ({ getService, getPageObjects }) {
expect(parseInt(await PageObjects.settings.getFieldsTabCount())).to.be(startingCount + 1);
});
});

it('should modify runtime field', async function () {
await PageObjects.settings.filterField(fieldName);
await testSubjects.click('editFieldFormat');
await PageObjects.settings.setFieldType('Long');
await PageObjects.settings.changeFieldScript('emit(6);');
await PageObjects.settings.clickSaveField();
await PageObjects.settings.confirmSave();
});

it('should delete runtime field', async function () {
await testSubjects.click('deleteField');
await PageObjects.settings.confirmDelete();
});
});
});
}
21 changes: 21 additions & 0 deletions test/functional/page_objects/settings_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,16 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
await this.closeIndexPatternFieldEditor();
}

public async confirmSave() {
await testSubjects.setValue('saveModalConfirmText', 'change');
await testSubjects.click('confirmModalConfirmButton');
}

public async confirmDelete() {
await testSubjects.setValue('deleteModalConfirmText', 'remove');
await testSubjects.click('confirmModalConfirmButton');
}

async closeIndexPatternFieldEditor() {
await retry.waitFor('field editor flyout to close', async () => {
return !(await testSubjects.exists('euiFlyoutCloseButton'));
Expand Down Expand Up @@ -543,6 +553,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
browser.pressKeys(script);
}

async changeFieldScript(script: string) {
log.debug('set script = ' + script);
const formatRow = await testSubjects.find('valueRow');
const getMonacoTextArea = async () => (await formatRow.findAllByCssSelector('textarea'))[0];
retry.waitFor('monaco editor is ready', async () => !!(await getMonacoTextArea()));
const monacoTextArea = await getMonacoTextArea();
await monacoTextArea.focus();
browser.pressKeys(browser.keys.DELETE.repeat(30));
browser.pressKeys(script);
}

async clickAddScriptedField() {
log.debug('click Add Scripted Field');
await testSubjects.click('addScriptedFieldLink');
Expand Down
Loading