Skip to content

Commit

Permalink
Index pattern field editor - Add warning and type 'confirm' on delete…
Browse files Browse the repository at this point in the history
… or save (#95237)

* add runtime field change/delete confirm dialog
  • Loading branch information
mattkime authored Mar 26, 2021
1 parent b9f5d0c commit ae4dae4
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 42 deletions.
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

0 comments on commit ae4dae4

Please sign in to comment.