From ae4dae46d93a0b7f6f2ce3941980e18b89050814 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Fri, 26 Mar 2021 09:03:13 -0500 Subject: [PATCH] Index pattern field editor - Add warning and type 'confirm' on delete or save (#95237) * add runtime field change/delete confirm dialog --- .../public/components/delete_field_modal.tsx | 74 ++++++++--- .../field_editor_flyout_content.test.ts | 2 +- .../field_editor_flyout_content.tsx | 116 +++++++++++++++--- .../apps/management/_runtime_fields.js | 15 +++ test/functional/page_objects/settings_page.ts | 21 ++++ test/functional/services/field_editor.ts | 16 ++- .../functional/apps/lens/runtime_fields.ts | 2 + .../test/functional/page_objects/lens_page.ts | 2 - 8 files changed, 206 insertions(+), 42 deletions(-) diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx index fca3eaf10b1ef..73a4837d6e0cc 100644 --- a/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_modal.tsx @@ -6,12 +6,13 @@ * 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; @@ -19,27 +20,35 @@ const geti18nTexts = (fieldsToDelete?: string[]) => { ? 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', { @@ -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.', + } + ), }; }; @@ -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(); return ( - {isMultiple && ( - <> -

{warningMultipleFields}

-
    - {fieldsToDelete.map((fieldName) => ( -
  • {fieldName}
  • - ))} -
- - )} + + {isMultiple && ( + <> +

{warningMultipleFields}

+
    + {fieldsToDelete.map((fieldName) => ( +
  • {fieldName}
  • + ))} +
+ + )} +
+ + + setConfirmContent(e.target.value)} + data-test-subj="deleteModalConfirmText" + /> +
); } diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts index e943dbdda998d..46414c264c6b7 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts @@ -68,7 +68,7 @@ describe('', () => { 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); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx index 1511836da85e7..486df1a7707af 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -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, @@ -19,6 +20,10 @@ import { EuiButton, EuiCallOut, EuiSpacer, + EuiText, + EuiConfirmModal, + EuiFieldText, + EuiFormRow, } from '@elastic/eui'; import { DocLinksStart, CoreStart } from 'src/core/public'; @@ -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', }), @@ -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:", + } + ), }; }; @@ -97,6 +117,7 @@ const FieldEditorFlyoutContentComponent = ({ runtimeFieldValidator, isSavingField, }: Props) => { + const isEditingExistingField = !!field; const i18nTexts = geti18nTexts(field); const [formState, setFormState] = useState({ @@ -112,6 +133,8 @@ const FieldEditorFlyoutContentComponent = ({ ); const [isValidating, setIsValidating] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [confirmContent, setConfirmContent] = useState(); const { submit, isValid: isFormValid, isSubmitted } = formState; const { fields } = indexPattern; @@ -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) { @@ -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]); @@ -180,12 +209,70 @@ const FieldEditorFlyoutContentComponent = ({ [fieldTypeToProcess, namesNotAllowed, existingConcreteFields] ); + const modal = isModalVisible ? ( + { + setIsModalVisible(false); + setConfirmContent(''); + }} + onConfirm={async () => { + const { data } = await submit(); + onSave(data); + }} + > + + + + setConfirmContent(e.target.value)} + data-test-subj="saveModalConfirmText" + /> + + + ) : null; return ( <> - -

{i18nTexts.flyoutTitle}

+ +

+ {field ? ( + + ) : ( + + )} +

+ +

+ {indexPattern.title}, + }} + /> +

+
@@ -246,6 +333,7 @@ const FieldEditorFlyoutContentComponent = ({ )} + {modal} ); }; diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.js index 4b3533f20c8dc..e3ff1819aed13 100644 --- a/test/functional/apps/management/_runtime_fields.js +++ b/test/functional/apps/management/_runtime_fields.js @@ -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']); @@ -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(); + }); }); }); } diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 4151a8c1a1893..14bd002ec9487 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -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')); @@ -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'); diff --git a/test/functional/services/field_editor.ts b/test/functional/services/field_editor.ts index 5cd1f2c4f6202..7d6dad4f7858e 100644 --- a/test/functional/services/field_editor.ts +++ b/test/functional/services/field_editor.ts @@ -10,7 +10,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function FieldEditorProvider({ getService }: FtrProviderContext) { const browser = getService('browser'); - const retry = getService('retry'); const testSubjects = getService('testSubjects'); class FieldEditor { @@ -33,10 +32,17 @@ export function FieldEditorProvider({ getService }: FtrProviderContext) { await browser.pressKeys(script); } public async save() { - await retry.try(async () => { - await testSubjects.click('fieldSaveButton'); - await testSubjects.missingOrFail('fieldSaveButton', { timeout: 2000 }); - }); + await testSubjects.click('fieldSaveButton'); + } + + public async confirmSave() { + await testSubjects.setValue('saveModalConfirmText', 'change'); + await testSubjects.click('confirmModalConfirmButton'); + } + + public async confirmDelete() { + await testSubjects.setValue('deleteModalConfirmText', 'remove'); + await testSubjects.click('confirmModalConfirmButton'); } } diff --git a/x-pack/test/functional/apps/lens/runtime_fields.ts b/x-pack/test/functional/apps/lens/runtime_fields.ts index 53fe9eab0e654..c5391f3f2b548 100644 --- a/x-pack/test/functional/apps/lens/runtime_fields.ts +++ b/x-pack/test/functional/apps/lens/runtime_fields.ts @@ -50,6 +50,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.editField(); await fieldEditor.setName('runtimefield2'); await fieldEditor.save(); + await fieldEditor.confirmSave(); await PageObjects.lens.searchField('runtime'); await PageObjects.lens.waitForField('runtimefield2'); await PageObjects.lens.dragFieldToDimensionTrigger( @@ -66,6 +67,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should able to remove field', async () => { await PageObjects.lens.clickField('runtimefield2'); await PageObjects.lens.removeField(); + await fieldEditor.confirmDelete(); await PageObjects.lens.waitForFieldMissing('runtimefield2'); }); }); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 45c6cca807b3f..ccc4ed4ad87d0 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -192,8 +192,6 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont await retry.try(async () => { await testSubjects.click('lnsFieldListPanelRemove'); await testSubjects.missingOrFail('lnsFieldListPanelRemove'); - await testSubjects.click('confirmModalConfirmButton'); - await testSubjects.missingOrFail('confirmModalConfirmButton'); }); },