Skip to content

Commit

Permalink
[ML] AIOps Log Rate Analysis: adds controls for controlling which col…
Browse files Browse the repository at this point in the history
…umns will be visible (#184262)

## Summary

Related meta issue: #182714

This PR adds controls to the AIOps results table to show/hide columns.

<img width="1100" alt="image"
src="https://github.com/elastic/kibana/assets/6446462/8e1f2913-614b-4fe2-884f-aa53760646e4">


### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
alvarezmelissa87 and kibanamachine committed Jun 10, 2024
1 parent 70df509 commit 2c76ad0
Show file tree
Hide file tree
Showing 18 changed files with 655 additions and 559 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface FieldFilterApplyButtonProps {
tooltipContent?: string;
}

export const FieldFilterApplyButton: FC<FieldFilterApplyButtonProps> = ({
export const ItemFilterApplyButton: FC<FieldFilterApplyButtonProps> = ({
disabled,
onClick,
tooltipContent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,40 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';

import { FieldFilterApplyButton } from './field_filter_apply_button';
import { ItemFilterApplyButton } from './item_filter_apply_button';

interface FieldFilterPopoverProps {
interface ItemFilterPopoverProps {
dataTestSubj: string;
disabled?: boolean;
disabledApplyButton?: boolean;
uniqueFieldNames: string[];
onChange: (skippedFields: string[]) => void;
disabledApplyTooltipContent?: string;
helpText: string;
itemSearchAriaLabel: string;
initialSkippedItems?: string[];
popoverButtonTitle: string;
selectedItemLimit?: number;
uniqueItemNames: string[];
onChange: (skippedItems: string[]) => void;
}

// This component is mostly inspired by EUI's Data Grid Column Selector
// https://github.com/elastic/eui/blob/main/src/components/datagrid/controls/column_selector.tsx
export const FieldFilterPopover: FC<FieldFilterPopoverProps> = ({
export const ItemFilterPopover: FC<ItemFilterPopoverProps> = ({
dataTestSubj,
disabled,
disabledApplyButton,
uniqueFieldNames,
disabledApplyTooltipContent,
helpText,
itemSearchAriaLabel,
initialSkippedItems = [],
popoverButtonTitle,
selectedItemLimit = 2,
uniqueItemNames,
onChange,
}) => {
const euiThemeContext = useEuiTheme();
// Inspired by https://github.com/elastic/eui/blob/main/src/components/datagrid/controls/_data_grid_column_selector.scss
const fieldSelectPopover = useMemo(
const itemSelectPopover = useMemo(
() => css`
${euiYScrollWithShadows(euiThemeContext, {})}
max-height: 400px;
Expand All @@ -55,97 +69,90 @@ export const FieldFilterPopover: FC<FieldFilterPopoverProps> = ({
);

const [isTouched, setIsTouched] = useState(false);
const [fieldSearchText, setFieldSearchText] = useState('');
const [skippedFields, setSkippedFields] = useState<string[]>([]);
const setFieldsFilter = (fieldNames: string[], checked: boolean) => {
let updatedSkippedFields = [...skippedFields];
const [itemSearchText, setItemSearchText] = useState('');
const [skippedItems, setSkippedItems] = useState<string[]>(initialSkippedItems);
const setItemsFilter = (itemNames: string[], checked: boolean) => {
let updatedSkippedItems = [...skippedItems];
if (!checked) {
updatedSkippedFields.push(...fieldNames);
updatedSkippedItems.push(...itemNames);
} else {
updatedSkippedFields = skippedFields.filter((d) => !fieldNames.includes(d));
updatedSkippedItems = skippedItems.filter((d) => !itemNames.includes(d));
}
setSkippedFields(updatedSkippedFields);
// Ensure there are no duplicates
setSkippedItems([...new Set(updatedSkippedItems)]);
setIsTouched(true);
};

const [isFieldSelectionPopoverOpen, setIsFieldSelectionPopoverOpen] = useState(false);
const onFieldSelectionButtonClick = () => setIsFieldSelectionPopoverOpen((isOpen) => !isOpen);
const closePopover = () => setIsFieldSelectionPopoverOpen(false);
const [isItemSelectionPopoverOpen, setIsItemSelectionPopoverOpen] = useState(false);
const onItemSelectionButtonClick = () => setIsItemSelectionPopoverOpen((isOpen) => !isOpen);
const closePopover = () => setIsItemSelectionPopoverOpen(false);

const filteredUniqueFieldNames = useMemo(() => {
return uniqueFieldNames.filter(
(d) => d.toLowerCase().indexOf(fieldSearchText.toLowerCase()) !== -1
const filteredUniqueItemNames = useMemo(() => {
return uniqueItemNames.filter(
(d) => d.toLowerCase().indexOf(itemSearchText.toLowerCase()) !== -1
);
}, [fieldSearchText, uniqueFieldNames]);
}, [itemSearchText, uniqueItemNames]);

// If the supplied list of unique field names changes, do a sanity check to only
// keep field names in the list of skipped fields that still are in the list of unique fields.
useEffect(() => {
setSkippedFields((previousSkippedFields) =>
previousSkippedFields.filter((d) => uniqueFieldNames.includes(d))
setSkippedItems((previousSkippedItems) =>
previousSkippedItems.filter((d) => uniqueItemNames.includes(d))
);
}, [uniqueFieldNames]);
}, [uniqueItemNames]);

const selectedFieldCount = uniqueFieldNames.length - skippedFields.length;
const selectedItemCount = uniqueItemNames.length - skippedItems.length;

return (
<EuiPopover
data-test-subj="aiopsFieldFilterPopover"
anchorPosition="downLeft"
panelPaddingSize="s"
panelStyle={{ minWidth: '20%' }}
button={
<EuiButton
data-test-subj="aiopsFieldFilterButton"
onClick={onFieldSelectionButtonClick}
data-test-subj={dataTestSubj}
onClick={onItemSelectionButtonClick}
disabled={disabled}
size="s"
iconType="arrowDown"
iconSide="right"
iconSize="s"
color="text"
>
<FormattedMessage
id="xpack.aiops.logRateAnalysis.page.fieldFilterButtonLabel"
defaultMessage="Filter fields"
/>
{popoverButtonTitle}
</EuiButton>
}
isOpen={isFieldSelectionPopoverOpen}
isOpen={isItemSelectionPopoverOpen}
closePopover={closePopover}
>
<EuiPopoverTitle>
<EuiText size="xs" color="subdued" style={{ maxWidth: '400px' }}>
<FormattedMessage
id="xpack.aiops.logRateAnalysis.page.fieldFilterHelpText"
defaultMessage="Deselect non-relevant fields to remove them from groups and click the Apply button to rerun the grouping. Use the search bar to filter the list, then select/deselect multiple fields with the actions below."
/>
{helpText}
</EuiText>
<EuiSpacer size="s" />
<EuiFieldText
compressed
placeholder={i18n.translate('xpack.aiops.analysis.fieldSelectorPlaceholder', {
defaultMessage: 'Search',
})}
aria-label={i18n.translate('xpack.aiops.analysis.fieldSelectorAriaLabel', {
defaultMessage: 'Filter fields',
})}
value={fieldSearchText}
onChange={(e: ChangeEvent<HTMLInputElement>) => setFieldSearchText(e.currentTarget.value)}
aria-label={itemSearchAriaLabel}
value={itemSearchText}
onChange={(e: ChangeEvent<HTMLInputElement>) => setItemSearchText(e.currentTarget.value)}
data-test-subj="aiopsFieldSelectorSearch"
/>
</EuiPopoverTitle>
<div css={fieldSelectPopover} data-test-subj="aiopsFieldSelectorFieldNameList">
{filteredUniqueFieldNames.map((fieldName) => (
<div css={itemSelectPopover} data-test-subj="aiopsFieldSelectorFieldNameList">
{filteredUniqueItemNames.map((fieldName) => (
<div key={fieldName} css={{ padding: '4px' }}>
<EuiSwitch
data-test-subj={`aiopsFieldSelectorFieldNameListItem${
!skippedFields.includes(fieldName) ? ' checked' : ''
!skippedItems.includes(fieldName) ? ' checked' : ''
}`}
className="euiSwitch--mini"
compressed
label={fieldName}
onChange={(e) => setFieldsFilter([fieldName], e.target.checked)}
checked={!skippedFields.includes(fieldName)}
onChange={(e) => setItemsFilter([fieldName], e.target.checked)}
checked={!skippedItems.includes(fieldName)}
/>
</div>
))}
Expand All @@ -162,19 +169,19 @@ export const FieldFilterPopover: FC<FieldFilterPopoverProps> = ({
<EuiButtonEmpty
size="xs"
flush="left"
onClick={() => setFieldsFilter(filteredUniqueFieldNames, true)}
disabled={fieldSearchText.length > 0 && filteredUniqueFieldNames.length === 0}
onClick={() => setItemsFilter(filteredUniqueItemNames, true)}
disabled={itemSearchText.length > 0 && filteredUniqueItemNames.length === 0}
data-test-subj="aiopsFieldSelectorSelectAllFieldsButton"
>
{fieldSearchText.length > 0 ? (
{itemSearchText.length > 0 ? (
<FormattedMessage
id="xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllSearchedFields"
defaultMessage="Select filtered fields"
id="xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllSearchedItems"
defaultMessage="Select filtered"
/>
) : (
<FormattedMessage
id="xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllFields"
defaultMessage="Select all fields"
id="xpack.aiops.logRateAnalysis.page.fieldSelector.selectAllItems"
defaultMessage="Select all"
/>
)}
</EuiButtonEmpty>
Expand All @@ -183,39 +190,35 @@ export const FieldFilterPopover: FC<FieldFilterPopoverProps> = ({
<EuiButtonEmpty
size="xs"
flush="right"
onClick={() => setFieldsFilter(filteredUniqueFieldNames, false)}
disabled={fieldSearchText.length > 0 && filteredUniqueFieldNames.length === 0}
onClick={() => setItemsFilter(filteredUniqueItemNames, false)}
disabled={itemSearchText.length > 0 && filteredUniqueItemNames.length === 0}
data-test-subj="aiopsFieldSelectorDeselectAllFieldsButton"
>
{fieldSearchText.length > 0 ? (
{itemSearchText.length > 0 ? (
<FormattedMessage
id="xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllSearchedFields"
defaultMessage="Deselect filtered fields"
id="xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllSearchedItems"
defaultMessage="Deselect filtered"
/>
) : (
<FormattedMessage
id="xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllFields"
defaultMessage="Deselect all fields"
id="xpack.aiops.logRateAnalysis.page.fieldSelector.deselectAllItems"
defaultMessage="Deselect all"
/>
)}
</EuiButtonEmpty>
</EuiFlexItem>
</>
<EuiFlexItem grow={false}>
<FieldFilterApplyButton
<ItemFilterApplyButton
onClick={() => {
onChange(skippedFields);
setFieldSearchText('');
setIsFieldSelectionPopoverOpen(false);
onChange(skippedItems);
setItemSearchText('');
setIsItemSelectionPopoverOpen(false);
closePopover();
}}
disabled={disabledApplyButton || selectedFieldCount < 2 || !isTouched}
disabled={disabledApplyButton || selectedItemCount < selectedItemLimit || !isTouched}
tooltipContent={
selectedFieldCount < 2
? i18n.translate('xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected', {
defaultMessage: 'Grouping requires at least 2 fields to be selected.',
})
: undefined
selectedItemCount < selectedItemLimit ? disabledApplyTooltipContent : undefined
}
/>
</EuiFlexItem>
Expand Down
Loading

0 comments on commit 2c76ad0

Please sign in to comment.