mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Fleet] Better input for multi text input in agent policy builder (#101020)
This commit is contained in:
parent
54c3ca142e
commit
eff776560e
3 changed files with 246 additions and 11 deletions
|
@ -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']);
|
||||
});
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue