diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/multi_text_input.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/multi_text_input.test.tsx new file mode 100644 index 0000000000000..18a7357b235b9 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/multi_text_input.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, act } from '@testing-library/react'; + +import { createTestRendererMock } from '../../../../mock'; + +import { MultiTextInput } from './multi_text_input'; + +function renderInput(value = ['value1']) { + const renderer = createTestRendererMock(); + const mockOnChange = jest.fn(); + + const utils = renderer.render(); + + return { utils, mockOnChange }; +} + +test('it should allow to add a new value', async () => { + const { utils, mockOnChange } = renderInput(); + + const addRowEl = await utils.findByText('Add row'); + fireEvent.click(addRowEl); + + expect(mockOnChange).toHaveBeenCalledWith(['value1']); + + const inputEl = await utils.findByDisplayValue(''); + expect(inputEl).toBeDefined(); + + fireEvent.change(inputEl, { target: { value: 'value2' } }); + + expect(mockOnChange).toHaveBeenCalledWith(['value1', 'value2']); +}); + +test('it should not show the delete button if there only one row', async () => { + const { utils } = renderInput(['value1']); + + await act(async () => { + const deleteRowEl = await utils.container.querySelector('[aria-label="Delete row"]'); + expect(deleteRowEl).toBeNull(); + }); +}); + +test('it should allow to update existing value', async () => { + const { utils, mockOnChange } = renderInput(['value1', 'value2']); + + const inputEl = await utils.findByDisplayValue('value1'); + expect(inputEl).toBeDefined(); + + fireEvent.change(inputEl, { target: { value: 'value1updated' } }); + + expect(mockOnChange).toHaveBeenCalledWith(['value1updated', 'value2']); +}); + +test('it should allow to remove a row', async () => { + const { utils, mockOnChange } = renderInput(['value1', 'value2']); + + await act(async () => { + const deleteRowEl = await utils.container.querySelector('[aria-label="Delete row"]'); + if (!deleteRowEl) { + throw new Error('Delete row button not found'); + } + fireEvent.click(deleteRowEl); + }); + + expect(mockOnChange).toHaveBeenCalledWith(['value2']); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/multi_text_input.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/multi_text_input.tsx new file mode 100644 index 0000000000000..e4e724199b493 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/multi_text_input.tsx @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback, useState, useEffect } from 'react'; +import type { FunctionComponent, ChangeEvent } from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiFieldText, + EuiButtonIcon, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + value: string[]; + onChange: (newValue: string[]) => void; + onBlur?: () => void; + errors?: Array<{ message: string; index?: number }>; + isInvalid?: boolean; + isDisabled?: boolean; +} + +interface RowProps { + index: number; + value: string; + onChange: (index: number, value: string) => void; + onDelete: (index: number) => void; + onBlur?: () => void; + autoFocus?: boolean; + isDisabled?: boolean; + showDeleteButton?: boolean; +} + +const Row: FunctionComponent = ({ + index, + value, + onChange, + onDelete, + onBlur, + autoFocus, + isDisabled, + showDeleteButton, +}) => { + const onDeleteHandler = useCallback(() => { + onDelete(index); + }, [onDelete, index]); + + const onChangeHandler = useCallback( + (e: ChangeEvent) => { + onChange(index, e.target.value); + }, + [onChange, index] + ); + + return ( + + + + + {showDeleteButton && ( + + + + )} + + ); +}; + +function defaultValue(value: string[]) { + return value.length > 0 ? value : ['']; +} + +export const MultiTextInput: FunctionComponent = ({ + value, + onChange, + onBlur, + isInvalid, + isDisabled, + errors, +}) => { + const [autoFocus, setAutoFocus] = useState(false); + const [rows, setRows] = useState(() => defaultValue(value)); + const [previousRows, setPreviousRows] = useState(rows); + + useEffect(() => { + if (previousRows === rows) { + return; + } + setPreviousRows(rows); + if (rows[rows.length - 1] === '') { + onChange(rows.slice(0, rows.length - 1)); + } else { + onChange(rows); + } + }, [onChange, previousRows, rows]); + + const onDeleteHandler = useCallback( + (idx: number) => { + setRows([...rows.slice(0, idx), ...rows.slice(idx + 1)]); + }, + [rows] + ); + + const onChangeHandler = useCallback( + (idx: number, newValue: string) => { + const newRows = [...rows]; + newRows[idx] = newValue; + setRows(newRows); + }, + [rows] + ); + + const addRowHandler = useCallback(() => { + setAutoFocus(true); + setRows([...rows, '']); + }, [rows]); + + return ( + <> + + {rows.map((row, idx) => ( + + 1} + /> + + ))} + + + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx index 7841e8bb62452..eed94de97113d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/package_policy_input_var_field.tsx @@ -12,7 +12,6 @@ import { EuiFormRow, EuiSwitch, EuiFieldText, - EuiComboBox, EuiText, EuiCodeEditor, EuiTextArea, @@ -23,6 +22,7 @@ import type { RegistryVarsEntry } from '../../../../types'; import 'brace/mode/yaml'; import 'brace/theme/textmate'; +import { MultiTextInput } from './multi_text_input'; export const PackagePolicyInputVarField: React.FunctionComponent<{ varDef: RegistryVarsEntry; @@ -41,16 +41,9 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{ const field = useMemo(() => { if (multi) { return ( - ({ label: val }))} - onCreateOption={(newVal: any) => { - onChange([...value, newVal]); - }} - onChange={(newVals: any[]) => { - onChange(newVals.map((val) => val.label)); - }} + setIsDirty(true)} isDisabled={frozen} />