From f77542ceb81f2f6ff152549830a38120171e9ee8 Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Tue, 23 Jan 2024 17:06:03 +0100 Subject: [PATCH] [Fleet] replace EuiFilterSelectItem with EuiSelectable (#175101) ## Summary Closes https://github.com/elastic/kibana/issues/162766 Replace deprecated EUI component. Agent details: select log level image Agent details: select dataset image Agent status filter: image Agent list view: Agent policy select: image Tags select: image ### Checklist Delete any items that are not applicable to this PR. - [x] 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) - [x] [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 --- .../fleet/cypress/e2e/agents/agent_list.cy.ts | 50 +++--- .../components/agent_logs/filter_dataset.tsx | 56 ++++-- .../agent_logs/filter_log_level.tsx | 38 ++-- .../components/agent_status_filter.test.tsx | 14 +- .../components/agent_status_filter.tsx | 87 ++++++---- .../filter_bar/agent_policy_filter.tsx | 111 ++++++++++++ .../components/filter_bar/tags_filter.tsx | 119 +++++++++++++ .../components/search_and_filter_bar.tsx | 164 ++---------------- 8 files changed, 402 insertions(+), 237 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/filter_bar/agent_policy_filter.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/filter_bar/tags_filter.tsx diff --git a/x-pack/plugins/fleet/cypress/e2e/agents/agent_list.cy.ts b/x-pack/plugins/fleet/cypress/e2e/agents/agent_list.cy.ts index a82c362a8ea4e..27a1098fe8256 100644 --- a/x-pack/plugins/fleet/cypress/e2e/agents/agent_list.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/agents/agent_list.cy.ts @@ -165,9 +165,9 @@ describe('View agents list', () => { cy.getBySel(FLEET_AGENT_LIST_PAGE.POLICY_FILTER).click(); - cy.get('button').contains('Agent policy 1'); - cy.get('button').contains('Agent policy 2'); - cy.get('button').contains('Agent policy 3'); + cy.get('li').contains('Agent policy 1'); + cy.get('li').contains('Agent policy 2'); + cy.get('li').contains('Agent policy 3'); }); it('should filter on single policy (no results)', () => { @@ -175,7 +175,7 @@ describe('View agents list', () => { cy.getBySel(FLEET_AGENT_LIST_PAGE.POLICY_FILTER).click(); - cy.get('button').contains('Agent policy 4').click(); + cy.get('li').contains('Agent policy 4').click(); assertTableIsEmpty(); }); @@ -185,7 +185,7 @@ describe('View agents list', () => { cy.getBySel(FLEET_AGENT_LIST_PAGE.POLICY_FILTER).click(); - cy.get('button').contains('Agent policy 1').click(); + cy.get('li').contains('Agent policy 1').click(); cy.getBySel(FLEET_AGENT_LIST_PAGE.TABLE).find('tr').should('have.length', 2); cy.getBySel(FLEET_AGENT_LIST_PAGE.TABLE).contains('agent-1'); @@ -196,8 +196,8 @@ describe('View agents list', () => { cy.getBySel(FLEET_AGENT_LIST_PAGE.POLICY_FILTER).click(); - cy.get('button').contains('Agent policy 1').click(); - cy.get('button').contains('Agent policy 2').click(); + cy.get('li').contains('Agent policy 1').click(); + cy.get('li').contains('Agent policy 2').click(); cy.getBySel(FLEET_AGENT_LIST_PAGE.TABLE).find('tr').should('have.length', 3); cy.getBySel(FLEET_AGENT_LIST_PAGE.TABLE).contains('agent-1'); @@ -208,10 +208,10 @@ describe('View agents list', () => { describe('Agent status filter', () => { const clearFilters = () => { cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click(); - cy.get('button').contains('Healthy').click(); - cy.get('button').contains('Unhealthy').click(); - cy.get('button').contains('Updating').click(); - cy.get('button').contains('Offline').click(); + cy.get('li').contains('Healthy').click(); + cy.get('li').contains('Unhealthy').click(); + cy.get('li').contains('Updating').click(); + cy.get('li').contains('Offline').click(); cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click(); cy.wait('@getAgents'); }; @@ -220,7 +220,7 @@ describe('View agents list', () => { clearFilters(); cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click(); - cy.get('button').contains('Healthy').click(); + cy.get('li').contains('Healthy').click(); cy.wait('@getAgents'); assertTableContainsNAgents(18); @@ -232,7 +232,7 @@ describe('View agents list', () => { clearFilters(); cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click(); - cy.get('button').contains('Unhealthy').click(); + cy.get('li').contains('Unhealthy').click(); cy.wait('@getAgents'); assertTableContainsNAgents(1); @@ -245,7 +245,7 @@ describe('View agents list', () => { cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click(); - cy.get('button').contains('Inactive').click(); + cy.get('li').contains('Inactive').click(); cy.getBySel(FLEET_AGENT_LIST_PAGE.TABLE).contains('No agents found'); }); @@ -256,8 +256,8 @@ describe('View agents list', () => { cy.getBySel(FLEET_AGENT_LIST_PAGE.STATUS_FILTER).click(); - cy.get('button').contains('Healthy').click(); - cy.get('button').contains('Unhealthy').click(); + cy.get('li').contains('Healthy').click(); + cy.get('li').contains('Unhealthy').click(); cy.wait('@getAgents'); assertTableContainsNAgents(18); @@ -270,7 +270,7 @@ describe('View agents list', () => { it('should allow to filter on one tag (tag1)', () => { cy.visit('/app/fleet/agents'); cy.getBySel(FLEET_AGENT_LIST_PAGE.TAGS_FILTER).click(); - cy.get('button').contains('tag1').click(); + cy.get('li').contains('tag1').click(); assertTableContainsNAgents(2); cy.getBySel(FLEET_AGENT_LIST_PAGE.TABLE).contains('agent-3'); @@ -280,8 +280,8 @@ describe('View agents list', () => { it('should allow to filter on multiple tag (tag1, tag2)', () => { cy.visit('/app/fleet/agents'); cy.getBySel(FLEET_AGENT_LIST_PAGE.TAGS_FILTER).click(); - cy.get('button').contains('tag1').click(); - cy.get('button').contains('tag2').click(); + cy.get('li').contains('tag1').click(); + cy.get('li').contains('tag2').click(); cy.wait('@getAgents'); assertTableContainsNAgents(4); @@ -294,8 +294,8 @@ describe('View agents list', () => { it('should allow to clear filters', () => { cy.visit('/app/fleet/agents'); cy.getBySel(FLEET_AGENT_LIST_PAGE.TAGS_FILTER).click(); - cy.get('button').contains('tag1').click(); - cy.get('button').contains('tag2').click(); + cy.get('li').contains('tag1').click(); + cy.get('li').contains('tag2').click(); cy.getBySel(FLEET_AGENT_LIST_PAGE.TAGS_FILTER).click(); assertTableContainsNAgents(4); @@ -311,7 +311,7 @@ describe('View agents list', () => { cy.getBySel(FLEET_AGENT_LIST_PAGE.POLICY_FILTER).click(); - cy.get('button').contains('Agent policy 3').click(); + cy.get('li').contains('Agent policy 3').click(); assertTableContainsNAgents(15); cy.getBySel(FLEET_AGENT_LIST_PAGE.CHECKBOX_SELECT_ALL).click(); @@ -345,7 +345,7 @@ describe('View agents list', () => { cy.getBySel(FLEET_AGENT_LIST_PAGE.POLICY_FILTER).click(); - cy.get('button').contains('Agent policy 3').click(); + cy.get('li').contains('Agent policy 3').click(); assertTableContainsNAgents(15); cy.getBySel(FLEET_AGENT_LIST_PAGE.CHECKBOX_SELECT_ALL).click(); // Trigger a bulk upgrade @@ -362,7 +362,7 @@ describe('View agents list', () => { cy.visit('/app/fleet/agents'); cy.getBySel(FLEET_AGENT_LIST_PAGE.POLICY_FILTER).click(); - cy.get('button').contains('Agent policy 3').click(); + cy.get('li').contains('Agent policy 3').click(); cy.wait('@getAgents'); assertTableContainsNAgents(15); cy.getBySel(FLEET_AGENT_LIST_PAGE.CHECKBOX_SELECT_ALL).click(); @@ -375,7 +375,7 @@ describe('View agents list', () => { assertTableIsEmpty(); // Select new policy is filters cy.getBySel(FLEET_AGENT_LIST_PAGE.POLICY_FILTER).click(); - cy.get('button').contains('Agent policy 4').click(); + cy.get('li').contains('Agent policy 4').click(); cy.wait('@getAgents'); assertTableContainsNAgents(15); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx index 9aafb11d999f1..aaa219e42832c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_dataset.tsx @@ -6,7 +6,8 @@ */ import React, { memo, useState, useEffect, useCallback } from 'react'; -import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { EuiPopover, EuiFilterButton, EuiSelectable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { DataViewField, FieldSpec } from '@kbn/data-views-plugin/public'; @@ -26,6 +27,20 @@ export const DatasetFilter: React.FunctionComponent<{ const togglePopover = useCallback(() => setIsOpen((prevIsOpen) => !prevIsOpen), [setIsOpen]); const closePopover = useCallback(() => setIsOpen(false), [setIsOpen]); + const datasetValuesToOptions = useCallback( + (values: string[]): EuiSelectableOption[] => { + return values.map((value) => ({ + label: value, + checked: selectedDatasets.includes(value) ? 'on' : undefined, + key: value, + })); + }, + [selectedDatasets] + ); + const [options, setOptions] = useState( + datasetValuesToOptions(datasetValues) + ); + useEffect(() => { const fetchValues = async () => { setIsLoading(true); @@ -47,14 +62,18 @@ export const DatasetFilter: React.FunctionComponent<{ field: DATASET_FIELD as DataViewField, query: '', }); - if (values.length > 0) setDatasetValues(values.sort()); + if (values.length > 0) { + setDatasetValues(values.sort()); + setOptions(datasetValuesToOptions(values.sort())); + } } catch (e) { setDatasetValues([AGENT_DATASET]); + setOptions(datasetValuesToOptions([AGENT_DATASET])); } setIsLoading(false); }; fetchValues(); - }, [data.dataViews, unifiedSearch.autocomplete]); + }, [data.dataViews, unifiedSearch.autocomplete, datasetValuesToOptions]); return ( - {datasetValues.map((dataset) => ( - onToggleDataset(dataset)} - > - {dataset} - - ))} + { + setOptions(newOptions); + newOptions.forEach((option, index) => { + if (option.checked !== options[index].checked) { + onToggleDataset(option.label); + return; + } + }); + }} + data-test-subj="agentList.datasetFilterOptions" + isLoading={isLoading} + listProps={{ + paddingSize: 's', + style: { + minWidth: 220, + }, + }} + > + {(list) => list} + ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx index 6b1f1060beb09..b4971ec4b2209 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/filter_log_level.tsx @@ -6,7 +6,8 @@ */ import React, { memo, useState, useCallback } from 'react'; -import { EuiPopover, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { EuiPopover, EuiFilterButton, EuiSelectable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AGENT_LOG_LEVELS } from './constants'; @@ -22,15 +23,13 @@ export const LogLevelFilter: React.FunctionComponent<{ const togglePopover = useCallback(() => setIsOpen((prevIsOpen) => !prevIsOpen), []); const closePopover = useCallback(() => setIsOpen(false), []); - const filterSelect = LEVEL_VALUES.map((level) => ( - onToggleLevel(level)} - > - {level} - - )); + const [options, setOptions] = useState( + LEVEL_VALUES.map((level) => ({ + label: level, + checked: selectedLevels.includes(level) ? 'on' : undefined, + key: level, + })) + ); return ( - {filterSelect} + { + setOptions(newOptions); + newOptions.forEach((option, index) => { + if (option.checked !== options[index].checked) { + onToggleLevel(option.label); + return; + } + }); + }} + data-test-subj="agentList.logLevelFilterOptions" + listProps={{ + paddingSize: 's', + }} + > + {(list) => list} + ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.test.tsx index 71853b29c3820..f8e3465557a81 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.test.tsx @@ -91,21 +91,21 @@ describe('AgentStatusFilter', () => { it('Should should show difference between last seen inactive agents and total agents', async () => { mockLocalStorage['fleet.lastSeenInactiveAgentsCount'] = '100'; - const { getByText, container } = renderComponent({ + const { getByText, getByTestId } = renderComponent({ selectedStatus: [], onSelectedStatusChange: () => {}, totalInactiveAgents: 999, }); await act(async () => { - const statusFilterButton = container.querySelector( - '[data-test-subj="agentList.statusFilter"]' - ); + const statusFilterButton = getByTestId('agentList.statusFilter'); - expect(statusFilterButton).not.toBeNull(); - fireEvent.click(statusFilterButton!); + fireEvent.click(statusFilterButton); - await waitFor(() => expect(getByText('899')).toBeInTheDocument()); + await waitFor(() => { + expect(getByTestId('agentList.agentStatusFilterOptions')).toBeInTheDocument(); + expect(getByText('899')).toBeInTheDocument(); + }); }); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.tsx index ece89842c1bf6..8134d0f9c54a0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_status_filter.tsx @@ -5,18 +5,19 @@ * 2.0. */ +import type { EuiSelectableOption } from '@elastic/eui'; +import { useEuiTheme } from '@elastic/eui'; import { EuiFilterButton, - EuiFilterSelectItem, EuiNotificationBadge, EuiPopover, + EuiSelectable, EuiText, EuiTourStep, - useEuiTheme, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { useInactiveAgentsCalloutHasBeenDismissed, useLastSeenInactiveAgentsCount } from '../hooks'; @@ -103,6 +104,7 @@ export const AgentStatusFilter: React.FC<{ totalInactiveAgents: number; isOpenByDefault?: boolean; }> = (props) => { + const { euiTheme } = useEuiTheme(); const { selectedStatus, onSelectedStatusChange, @@ -155,7 +157,43 @@ export const AgentStatusFilter: React.FC<{ setIsStatusFilterOpen(isOpen); }; - const { euiTheme } = useEuiTheme(); + const getOptions = useCallback((): EuiSelectableOption[] => { + return statusFilters.map(({ label, status }) => { + return { + label, + checked: selectedStatus.includes(status) ? 'on' : undefined, + key: status, + append: + status === 'inactive' && newlyInactiveAgentsCount > 0 ? ( + {newlyInactiveAgentsCount} + ) : undefined, + }; + }); + }, [selectedStatus, newlyInactiveAgentsCount]); + + const [options, setOptions] = useState(getOptions()); + + useEffect(() => { + setOptions(getOptions()); + }, [getOptions]); + + const onOptionsChange = useCallback( + (newOptions: EuiSelectableOption[]) => { + setOptions(newOptions); + newOptions.forEach((option, index) => { + if (option.checked !== options[index].checked) { + const status = option.key!; + if (option.checked !== 'on') { + onSelectedStatusChange([...selectedStatus.filter((s) => s !== status)]); + } else { + onSelectedStatusChange([...selectedStatus, status]); + } + return; + } + }); + }, + [onSelectedStatusChange, options, selectedStatus] + ); return ( updateIsStatusFilterOpen(false)} panelPaddingSize="none" > - {/* EUI NOTE: Please use EuiSelectable (which already has height/scrolling built in) - instead of EuiFilterSelectItem (which is pending deprecation). - @see https://elastic.github.io/eui/#/forms/filter-group#multi-select */} -
- {statusFilters.map(({ label, status }, idx) => ( - { - if (selectedStatus.includes(status)) { - onSelectedStatusChange([...selectedStatus.filter((s) => s !== status)]); - } else { - onSelectedStatusChange([...selectedStatus, status]); - } - }} - > - - {label} - {status === 'inactive' && newlyInactiveAgentsCount > 0 && ( - - {newlyInactiveAgentsCount} - - )} - - - ))} -
+ + {(list) => list} +
); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/filter_bar/agent_policy_filter.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/filter_bar/agent_policy_filter.tsx new file mode 100644 index 0000000000000..afc970b5754ec --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/filter_bar/agent_policy_filter.tsx @@ -0,0 +1,111 @@ +/* + * 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, useEffect, useState } from 'react'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { useEuiTheme } from '@elastic/eui'; +import { EuiFilterButton, EuiPopover, EuiSelectable } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { AgentPolicy } from '../../../../../../../../common'; + +export interface Props { + selectedAgentPolicies: string[]; + onSelectedAgentPoliciesChange: (selectedPolicies: string[]) => void; + agentPolicies: AgentPolicy[]; +} + +export const AgentPolicyFilter: React.FunctionComponent = ({ + selectedAgentPolicies, + onSelectedAgentPoliciesChange, + agentPolicies, +}: Props) => { + const { euiTheme } = useEuiTheme(); + // Policies state for filtering + const [isAgentPoliciesFilterOpen, setIsAgentPoliciesFilterOpen] = useState(false); + + // Add a agent policy id to current search + const addAgentPolicyFilter = (policyId: string) => { + onSelectedAgentPoliciesChange([...selectedAgentPolicies, policyId]); + }; + + // Remove a agent policy id from current search + const removeAgentPolicyFilter = (policyId: string) => { + onSelectedAgentPoliciesChange( + selectedAgentPolicies.filter((agentPolicy) => agentPolicy !== policyId) + ); + }; + + const getOptions = useCallback((): EuiSelectableOption[] => { + return agentPolicies.map((agentPolicy) => ({ + label: agentPolicy.name, + checked: selectedAgentPolicies.includes(agentPolicy.id) ? 'on' : undefined, + key: agentPolicy.id, + 'data-test-subj': 'agentList.agentPolicyFilterOption', + })); + }, [agentPolicies, selectedAgentPolicies]); + + const [options, setOptions] = useState(getOptions()); + + useEffect(() => { + setOptions(getOptions()); + }, [getOptions]); + + return ( + setIsAgentPoliciesFilterOpen(!isAgentPoliciesFilterOpen)} + isSelected={isAgentPoliciesFilterOpen} + hasActiveFilters={selectedAgentPolicies.length > 0} + numActiveFilters={selectedAgentPolicies.length} + numFilters={agentPolicies.length} + disabled={agentPolicies.length === 0} + data-test-subj="agentList.policyFilter" + > + + + } + isOpen={isAgentPoliciesFilterOpen} + closePopover={() => setIsAgentPoliciesFilterOpen(false)} + panelPaddingSize="none" + > + { + setOptions(newOptions); + newOptions.forEach((option, index) => { + if (option.checked !== options[index].checked) { + const agentPolicyId = option.key!; + if (option.checked !== 'on') { + removeAgentPolicyFilter(agentPolicyId); + } else { + addAgentPolicyFilter(agentPolicyId); + } + return; + } + }); + }} + data-test-subj="agentList.agentPolicyFilterOptions" + listProps={{ + paddingSize: 's', + style: { + minWidth: 200, + }, + }} + > + {(list) => list} + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/filter_bar/tags_filter.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/filter_bar/tags_filter.tsx new file mode 100644 index 0000000000000..d7a61170b13c9 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/filter_bar/tags_filter.tsx @@ -0,0 +1,119 @@ +/* + * 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, useEffect, useState } from 'react'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; +import { EuiHorizontalRule } from '@elastic/eui'; +import { EuiFilterButton, EuiPopover, EuiSelectable } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface Props { + tags: string[]; + selectedTags: string[]; + onSelectedTagsChange: (selectedTags: string[]) => void; +} + +export const TagsFilter: React.FunctionComponent = ({ + tags, + selectedTags, + onSelectedTagsChange, +}: Props) => { + const { euiTheme } = useEuiTheme(); + const [isTagsFilterOpen, setIsTagsFilterOpen] = useState(false); + + const addTagsFilter = (tag: string) => { + onSelectedTagsChange([...selectedTags, tag]); + }; + + const removeTagsFilter = (tag: string) => { + onSelectedTagsChange(selectedTags.filter((t) => t !== tag)); + }; + + const getOptions = useCallback((): EuiSelectableOption[] => { + return tags.map((tag) => ({ + label: tag, + checked: selectedTags.includes(tag) ? 'on' : undefined, + key: tag, + 'data-test-subj': 'agentList.tagFilterOption', + })); + }, [tags, selectedTags]); + + const [options, setOptions] = useState(getOptions()); + + useEffect(() => { + setOptions(getOptions()); + }, [getOptions]); + + return ( + setIsTagsFilterOpen(!isTagsFilterOpen)} + isSelected={isTagsFilterOpen} + hasActiveFilters={selectedTags.length > 0} + numActiveFilters={selectedTags.length} + numFilters={tags.length} + disabled={tags.length === 0} + data-test-subj="agentList.tagsFilter" + > + + + } + isOpen={isTagsFilterOpen} + closePopover={() => setIsTagsFilterOpen(false)} + panelPaddingSize="none" + > + { + setOptions(newOptions); + newOptions.forEach((option, index) => { + if (option.checked !== options[index].checked) { + const tag = option.key!; + if (option.checked !== 'on') { + removeTagsFilter(tag); + } else { + addTagsFilter(tag); + } + return; + } + }); + }} + data-test-subj="agentList.agentPolicyFilterOptions" + listProps={{ + paddingSize: 's', + style: { + minWidth: 140, + }, + }} + > + {(list) => list} + + + + + { + onSelectedTagsChange([]); + }} + > + + + + + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx index 99ca44a8f7e6b..3ecc5e6545795 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx @@ -5,22 +5,16 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { EuiButton, EuiFilterButton, EuiFilterGroup, - EuiFilterSelectItem, EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, - EuiIcon, - EuiPopover, EuiToolTip, - useEuiTheme, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import styled from 'styled-components'; import { useIsFirstTimeAgentUserQuery } from '../../../../../integrations/sections/epm/screens/detail/hooks'; @@ -29,17 +23,13 @@ import { SearchBar } from '../../../../components'; import { AGENTS_INDEX, AGENTS_PREFIX } from '../../../../constants'; import { useFleetServerStandalone } from '../../../../hooks'; -import { MAX_TAG_DISPLAY_LENGTH, truncateTag } from '../utils'; - import { AgentBulkActions } from './bulk_actions'; import type { SelectionMode } from './types'; import { AgentActivityButton } from './agent_activity_button'; import { AgentStatusFilter } from './agent_status_filter'; import { DashboardsButtons } from './dashboards_buttons'; - -const ClearAllTagsFilterItem = styled(EuiFilterSelectItem)` - padding: ${(props) => props.theme.eui.euiSizeS}; -`; +import { AgentPolicyFilter } from './filter_bar/agent_policy_filter'; +import { TagsFilter } from './filter_bar/tags_filter'; export interface SearchAndFilterBarProps { agentPolicies: AgentPolicy[]; @@ -98,37 +88,11 @@ export const SearchAndFilterBar: React.FunctionComponent { - const { euiTheme } = useEuiTheme(); const { isFleetServerStandalone } = useFleetServerStandalone(); const { isFirstTimeAgentUser, isLoading: isFirstTimeAgentUserLoading } = useIsFirstTimeAgentUserQuery(); const showAddFleetServerBtn = !isFleetServerStandalone; - // Policies state for filtering - const [isAgentPoliciesFilterOpen, setIsAgentPoliciesFilterOpen] = useState(false); - - const [isTagsFilterOpen, setIsTagsFilterOpen] = useState(false); - - // Add a agent policy id to current search - const addAgentPolicyFilter = (policyId: string) => { - onSelectedAgentPoliciesChange([...selectedAgentPolicies, policyId]); - }; - - // Remove a agent policy id from current search - const removeAgentPolicyFilter = (policyId: string) => { - onSelectedAgentPoliciesChange( - selectedAgentPolicies.filter((agentPolicy) => agentPolicy !== policyId) - ); - }; - - const addTagsFilter = (tag: string) => { - onSelectedTagsChange([...selectedTags, tag]); - }; - - const removeTagsFilter = (tag: string) => { - onSelectedTagsChange(selectedTags.filter((t) => t !== tag)); - }; - return ( <> @@ -207,118 +171,16 @@ export const SearchAndFilterBar: React.FunctionComponent - setIsTagsFilterOpen(!isTagsFilterOpen)} - isSelected={isTagsFilterOpen} - hasActiveFilters={selectedTags.length > 0} - numActiveFilters={selectedTags.length} - numFilters={tags.length} - disabled={tags.length === 0} - data-test-subj="agentList.tagsFilter" - > - - - } - isOpen={isTagsFilterOpen} - closePopover={() => setIsTagsFilterOpen(false)} - panelPaddingSize="none" - > - {/* EUI NOTE: Please use EuiSelectable (which already has height/scrolling built in) - instead of EuiFilterSelectItem (which is pending deprecation). - @see https://elastic.github.io/eui/#/forms/filter-group#multi-select */} -
- <> - {tags.map((tag, index) => ( - { - if (selectedTags.includes(tag)) { - removeTagsFilter(tag); - } else { - addTagsFilter(tag); - } - }} - > - {tag.length > MAX_TAG_DISPLAY_LENGTH ? ( - - {truncateTag(tag)} - - ) : ( - tag - )} - - ))} - - - - { - onSelectedTagsChange([]); - }} - > - - - - - Clear all - - - -
-
- setIsAgentPoliciesFilterOpen(!isAgentPoliciesFilterOpen)} - isSelected={isAgentPoliciesFilterOpen} - hasActiveFilters={selectedAgentPolicies.length > 0} - numActiveFilters={selectedAgentPolicies.length} - numFilters={agentPolicies.length} - disabled={agentPolicies.length === 0} - data-test-subj="agentList.policyFilter" - > - - - } - isOpen={isAgentPoliciesFilterOpen} - closePopover={() => setIsAgentPoliciesFilterOpen(false)} - panelPaddingSize="none" - > - {/* EUI NOTE: Please use EuiSelectable (which already has height/scrolling built in) - instead of EuiFilterSelectItem (which is pending deprecation). - @see https://elastic.github.io/eui/#/forms/filter-group#multi-select */} -
- {agentPolicies.map((agentPolicy, index) => ( - { - if (selectedAgentPolicies.includes(agentPolicy.id)) { - removeAgentPolicyFilter(agentPolicy.id); - } else { - addAgentPolicyFilter(agentPolicy.id); - } - }} - > - {agentPolicy.name} - - ))} -
-
+ + {