mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Index pattern field editor - Add warning and type 'confirm' on delete or save (#95237)
* add runtime field change/delete confirm dialog
This commit is contained in:
parent
b9f5d0c2e1
commit
ae4dae46d9
8 changed files with 206 additions and 42 deletions
|
@ -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<string>();
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
title={modalTitle}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<FieldEditorFormState>({
|
||||
|
@ -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;
|
||||
|
@ -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 ? (
|
||||
<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>
|
||||
|
@ -246,6 +333,7 @@ const FieldEditorFlyoutContentComponent = ({
|
|||
</>
|
||||
)}
|
||||
</EuiFlyoutFooter>
|
||||
{modal}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue