Skip to content

Commit

Permalink
[Fleet] Better input for multi text input in agent policy builder (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
nchaulet committed Jun 1, 2021
1 parent 54c3ca1 commit eff7765
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -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']);
});
Original file line number Diff line number Diff line change
@@ -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>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
EuiFormRow,
EuiSwitch,
EuiFieldText,
EuiComboBox,
EuiText,
EuiCodeEditor,
EuiTextArea,
Expand All @@ -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;
Expand All @@ -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}
/>
Expand Down

0 comments on commit eff7765

Please sign in to comment.