[Fleet] Better input for multi text input in agent policy builder (#101020) (#101077)

Co-authored-by: Nicolas Chaulet <nicolas.chaulet@elastic.co>
This commit is contained in:
Kibana Machine 2021-06-01 15:27:33 -04:00 committed by GitHub
parent 09129c8e38
commit b2766e9730
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 246 additions and 11 deletions

View file

@ -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(<MultiTextInput value={value} onChange={mockOnChange} />);
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']);
});

View file

@ -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<RowProps> = ({
index,
value,
onChange,
onDelete,
onBlur,
autoFocus,
isDisabled,
showDeleteButton,
}) => {
const onDeleteHandler = useCallback(() => {
onDelete(index);
}, [onDelete, index]);
const onChangeHandler = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange(index, e.target.value);
},
[onChange, index]
);
return (
<EuiFlexGroup alignItems="center" gutterSize="none" responsive={false}>
<EuiFlexItem>
<EuiFieldText
fullWidth
value={value}
onChange={onChangeHandler}
autoFocus={autoFocus}
disabled={isDisabled}
onBlur={onBlur}
/>
</EuiFlexItem>
{showDeleteButton && (
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="text"
onClick={onDeleteHandler}
iconType="cross"
disabled={isDisabled}
aria-label={i18n.translate('xpack.fleet.multiTextInput.deleteRowButton', {
defaultMessage: 'Delete row',
})}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};
function defaultValue(value: string[]) {
return value.length > 0 ? value : [''];
}
export const MultiTextInput: FunctionComponent<Props> = ({
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 (
<>
<EuiFlexGroup gutterSize="s" direction="column">
{rows.map((row, idx) => (
<EuiFlexItem key={idx}>
<Row
index={idx}
onChange={onChangeHandler}
onDelete={onDeleteHandler}
onBlur={onBlur}
value={row}
autoFocus={autoFocus}
isDisabled={isDisabled}
showDeleteButton={rows.length > 1}
/>
</EuiFlexItem>
))}
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiButtonEmpty
disabled={isDisabled}
size="xs"
flush="left"
iconType="plusInCircle"
onClick={addRowHandler}
>
<FormattedMessage id="xpack.fleet.multiTextInput.addRow" defaultMessage="Add row" />
</EuiButtonEmpty>
</>
);
};

View file

@ -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 (
<EuiComboBox
noSuggestions
isInvalid={isInvalid}
selectedOptions={value.map((val: string) => ({ label: val }))}
onCreateOption={(newVal: any) => {
onChange([...value, newVal]);
}}
onChange={(newVals: any[]) => {
onChange(newVals.map((val) => val.label));
}}
<MultiTextInput
value={value}
onChange={onChange}
onBlur={() => setIsDirty(true)}
isDisabled={frozen}
/>