Skip to content

Commit

Permalink
Merge branch 'main' into BUG-Inspect-Modal-Title
Browse files Browse the repository at this point in the history
  • Loading branch information
christineweng authored Oct 19, 2022
2 parents b6cb5ce + 6c5d816 commit c81bff6
Show file tree
Hide file tree
Showing 80 changed files with 5,754 additions and 3,670 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,30 @@ describe('FieldComponent', () => {
expect(wrapper.getByTestId('fieldAutocompleteComboBox')).toHaveTextContent('_source')
);
});

it('it allows custom user input if "acceptsCustomOptions" is "true"', async () => {
const mockOnChange = jest.fn();
const wrapper = render(
<FieldComponent
indexPattern={{
fields,
id: '1234',
title: 'logstash-*',
}}
isClearable={false}
isDisabled={false}
isLoading={false}
onChange={mockOnChange}
placeholder="Placeholder text"
selectedField={undefined}
acceptsCustomOptions
/>
);

const fieldAutocompleteComboBox = wrapper.getByTestId('comboBoxSearchInput');
fireEvent.change(fieldAutocompleteComboBox, { target: { value: 'custom' } });
await waitFor(() =>
expect(wrapper.getByTestId('fieldAutocompleteComboBox')).toHaveTextContent('custom')
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,18 @@ describe('useField', () => {
]);
});
});
it('should invoke onChange with custom option if one is sent', () => {
const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock }));
act(() => {
result.current.handleCreateCustomOption('madeUpField');
expect(onChangeMock).toHaveBeenCalledWith([
{
name: 'madeUpField',
type: 'text',
},
]);
});
});
});

describe('fieldWidth', () => {
Expand Down
25 changes: 25 additions & 0 deletions packages/kbn-securitysolution-autocomplete/src/field/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const FieldComponent: React.FC<FieldProps> = ({
onChange,
placeholder,
selectedField,
acceptsCustomOptions = false,
}): JSX.Element => {
const {
isInvalid,
Expand All @@ -35,6 +36,7 @@ export const FieldComponent: React.FC<FieldProps> = ({
renderFields,
handleTouch,
handleValuesChange,
handleCreateCustomOption,
} = useField({
indexPattern,
fieldTypeFilter,
Expand All @@ -43,6 +45,29 @@ export const FieldComponent: React.FC<FieldProps> = ({
fieldInputWidth,
onChange,
});

if (acceptsCustomOptions) {
return (
<EuiComboBox
placeholder={placeholder}
options={comboOptions}
selectedOptions={selectedComboOptions}
onChange={handleValuesChange}
isLoading={isLoading}
isDisabled={isDisabled}
isClearable={isClearable}
isInvalid={isInvalid}
onFocus={handleTouch}
singleSelection={AS_PLAIN_TEXT}
data-test-subj="fieldAutocompleteComboBox"
style={fieldWidth}
onCreateOption={handleCreateCustomOption}
customOptionText="Add {searchValue} as your occupation"
fullWidth
/>
);
}

return (
<EuiComboBox
placeholder={placeholder}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface FieldProps extends FieldBaseProps {
isDisabled: boolean;
isLoading: boolean;
placeholder: string;
acceptsCustomOptions?: boolean;
}
export interface FieldBaseProps {
indexPattern: DataViewBase | undefined;
Expand Down
27 changes: 23 additions & 4 deletions packages/kbn-securitysolution-autocomplete/src/field/use_field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,15 @@ export const useField = ({
}: FieldBaseProps) => {
const [touched, setIsTouched] = useState(false);

const { availableFields, selectedFields } = useMemo(
() => getComboBoxFields(indexPattern, selectedField, fieldTypeFilter),
[indexPattern, fieldTypeFilter, selectedField]
);
const [customOption, setCustomOption] = useState<DataViewFieldBase | null>(null);

const { availableFields, selectedFields } = useMemo(() => {
const indexPatternsToUse =
customOption != null && indexPattern != null
? { ...indexPattern, fields: [...indexPattern?.fields, customOption] }
: indexPattern;
return getComboBoxFields(indexPatternsToUse, selectedField, fieldTypeFilter);
}, [indexPattern, fieldTypeFilter, selectedField, customOption]);

const { comboOptions, labels, selectedComboOptions, disabledLabelTooltipTexts } = useMemo(
() => getComboBoxProps({ availableFields, selectedFields }),
Expand All @@ -117,6 +122,19 @@ export const useField = ({
[availableFields, labels, onChange]
);

const handleCreateCustomOption = useCallback(
(val: string) => {
const normalizedSearchValue = val.trim().toLowerCase();

if (!normalizedSearchValue) {
return;
}
setCustomOption({ name: val, type: 'text' });
onChange([{ name: val, type: 'text' }]);
},
[onChange]
);

const handleTouch = useCallback((): void => {
setIsTouched(true);
}, [setIsTouched]);
Expand Down Expand Up @@ -161,5 +179,6 @@ export const useField = ({
renderFields,
handleTouch,
handleValuesChange,
handleCreateCustomOption,
};
};
136 changes: 76 additions & 60 deletions packages/kbn-securitysolution-list-utils/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
EntriesArray,
Entry,
EntryNested,
ExceptionListItemSchema,
ExceptionListType,
ListSchema,
NamespaceType,
Expand All @@ -27,6 +26,8 @@ import {
entry,
exceptionListItemSchema,
nestedEntryItem,
CreateRuleExceptionListItemSchema,
createRuleExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import {
DataViewBase,
Expand Down Expand Up @@ -55,6 +56,7 @@ import {
EmptyEntry,
EmptyNestedEntry,
ExceptionsBuilderExceptionItem,
ExceptionsBuilderReturnExceptionItem,
FormattedBuilderEntry,
OperatorOption,
} from '../types';
Expand All @@ -65,59 +67,60 @@ export const isEntryNested = (item: BuilderEntry): item is EntryNested => {

export const filterExceptionItems = (
exceptions: ExceptionsBuilderExceptionItem[]
): Array<ExceptionListItemSchema | CreateExceptionListItemSchema> => {
return exceptions.reduce<Array<ExceptionListItemSchema | CreateExceptionListItemSchema>>(
(acc, exception) => {
const entries = exception.entries.reduce<BuilderEntry[]>((nestedAcc, singleEntry) => {
const strippedSingleEntry = removeIdFromItem(singleEntry);

if (entriesNested.is(strippedSingleEntry)) {
const nestedEntriesArray = strippedSingleEntry.entries.filter((singleNestedEntry) => {
const noIdSingleNestedEntry = removeIdFromItem(singleNestedEntry);
const [validatedNestedEntry] = validate(noIdSingleNestedEntry, nestedEntryItem);
return validatedNestedEntry != null;
});
const noIdNestedEntries = nestedEntriesArray.map((singleNestedEntry) =>
removeIdFromItem(singleNestedEntry)
);

const [validatedNestedEntry] = validate(
{ ...strippedSingleEntry, entries: noIdNestedEntries },
entriesNested
);

if (validatedNestedEntry != null) {
return [...nestedAcc, { ...singleEntry, entries: nestedEntriesArray }];
}
return nestedAcc;
} else {
const [validatedEntry] = validate(strippedSingleEntry, entry);

if (validatedEntry != null) {
return [...nestedAcc, singleEntry];
}
return nestedAcc;
): ExceptionsBuilderReturnExceptionItem[] => {
return exceptions.reduce<ExceptionsBuilderReturnExceptionItem[]>((acc, exception) => {
const entries = exception.entries.reduce<BuilderEntry[]>((nestedAcc, singleEntry) => {
const strippedSingleEntry = removeIdFromItem(singleEntry);
if (entriesNested.is(strippedSingleEntry)) {
const nestedEntriesArray = strippedSingleEntry.entries.filter((singleNestedEntry) => {
const noIdSingleNestedEntry = removeIdFromItem(singleNestedEntry);
const [validatedNestedEntry] = validate(noIdSingleNestedEntry, nestedEntryItem);
return validatedNestedEntry != null;
});
const noIdNestedEntries = nestedEntriesArray.map((singleNestedEntry) =>
removeIdFromItem(singleNestedEntry)
);

const [validatedNestedEntry] = validate(
{ ...strippedSingleEntry, entries: noIdNestedEntries },
entriesNested
);

if (validatedNestedEntry != null) {
return [...nestedAcc, { ...singleEntry, entries: nestedEntriesArray }];
}
}, []);

if (entries.length === 0) {
return acc;
return nestedAcc;
} else {
const [validatedEntry] = validate(strippedSingleEntry, entry);
if (validatedEntry != null) {
return [...nestedAcc, singleEntry];
}
return nestedAcc;
}
}, []);

const item = { ...exception, entries };
if (entries.length === 0) {
return acc;
}

if (exceptionListItemSchema.is(item)) {
return [...acc, item];
} else if (createExceptionListItemSchema.is(item)) {
const { meta, ...rest } = item;
const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined };
return [...acc, itemSansMetaId];
} else {
return acc;
}
},
[]
);
const item = { ...exception, entries };

if (exceptionListItemSchema.is(item)) {
return [...acc, item];
} else if (
createExceptionListItemSchema.is(item) ||
createRuleExceptionListItemSchema.is(item)
) {
const { meta, ...rest } = item;
const itemSansMetaId: CreateExceptionListItemSchema | CreateRuleExceptionListItemSchema = {
...rest,
meta: undefined,
};
return [...acc, itemSansMetaId];
} else {
return acc;
}
}, []);
};

export const addIdToEntries = (entries: EntriesArray): EntriesArray => {
Expand All @@ -136,15 +139,15 @@ export const addIdToEntries = (entries: EntriesArray): EntriesArray => {
export const getNewExceptionItem = ({
listId,
namespaceType,
ruleName,
name,
}: {
listId: string | undefined;
namespaceType: NamespaceType | undefined;
ruleName: string;
name: string;
}): CreateExceptionListItemBuilderSchema => {
return {
comments: [],
description: 'Exception list item',
description: `Exception list item`,
entries: addIdToEntries([
{
field: '',
Expand All @@ -158,7 +161,7 @@ export const getNewExceptionItem = ({
meta: {
temporaryUuid: uuid.v4(),
},
name: `${ruleName} - exception list item`,
name,
namespace_type: namespaceType,
tags: [],
type: 'simple',
Expand Down Expand Up @@ -769,13 +772,15 @@ export const getCorrespondingKeywordField = ({
* @param parent nested entries hold copy of their parent for use in various logic
* @param parentIndex corresponds to the entry index, this might seem obvious, but
* was added to ensure that nested items could be identified with their parent entry
* @param allowCustomFieldOptions determines if field must be found to match in indexPattern or not
*/
export const getFormattedBuilderEntry = (
indexPattern: DataViewBase,
item: BuilderEntry,
itemIndex: number,
parent: EntryNested | undefined,
parentIndex: number | undefined
parentIndex: number | undefined,
allowCustomFieldOptions: boolean
): FormattedBuilderEntry => {
const { fields } = indexPattern;
const field = parent != null ? `${parent.field}.${item.field}` : item.field;
Expand All @@ -800,10 +805,14 @@ export const getFormattedBuilderEntry = (
value: getEntryValue(item),
};
} else {
const fieldToUse = allowCustomFieldOptions
? foundField ?? { name: item.field, type: 'keyword' }
: foundField;

return {
correspondingKeywordField,
entryIndex: itemIndex,
field: foundField,
field: fieldToUse,
id: item.id != null ? item.id : `${itemIndex}`,
nested: undefined,
operator: getExceptionOperatorSelect(item),
Expand All @@ -819,15 +828,15 @@ export const getFormattedBuilderEntry = (
*
* @param patterns DataViewBase containing available fields on rule index
* @param entries exception item entries
* @param addNested boolean noting whether or not UI is currently
* set to add a nested field
* @param allowCustomFieldOptions determines if field must be found to match in indexPattern or not
* @param parent nested entries hold copy of their parent for use in various logic
* @param parentIndex corresponds to the entry index, this might seem obvious, but
* was added to ensure that nested items could be identified with their parent entry
*/
export const getFormattedBuilderEntries = (
indexPattern: DataViewBase,
entries: BuilderEntry[],
allowCustomFieldOptions: boolean,
parent?: EntryNested,
parentIndex?: number
): FormattedBuilderEntry[] => {
Expand All @@ -839,7 +848,8 @@ export const getFormattedBuilderEntries = (
item,
index,
parent,
parentIndex
parentIndex,
allowCustomFieldOptions
);
return [...acc, newItemEntry];
} else {
Expand Down Expand Up @@ -869,7 +879,13 @@ export const getFormattedBuilderEntries = (
}

if (isEntryNested(item)) {
const nestedItems = getFormattedBuilderEntries(indexPattern, item.entries, item, index);
const nestedItems = getFormattedBuilderEntries(
indexPattern,
item.entries,
allowCustomFieldOptions,
item,
index
);

return [...acc, parentEntry, ...nestedItems];
}
Expand Down
Loading

0 comments on commit c81bff6

Please sign in to comment.