From 7e8ee65c63ad9810da51ca75d6dbd96e560bc7cf Mon Sep 17 00:00:00 2001 From: Jill Guyonnet Date: Tue, 12 Mar 2024 08:03:36 +0000 Subject: [PATCH] [Fleet] Actions menu for Fleet agents has correct agent count and bulk actions are correctly processed for all selected agents (#177035) ## Summary Closes https://github.com/elastic/kibana/issues/167269 Closes https://github.com/elastic/kibana/issues/171914 Closes https://github.com/elastic/kibana/issues/167241 Changes: - Agent count in `Actions` menu includes all selectable agents across all pages, including agents with inactive status. - `Actions` menu items are enabled if at least one agent is selected, no matter its status. - Fix bug where managed agents could be accidentally selected in query mode when changing filtering. - Changing agent status or agent policy filtering while in bulk selection mode sets selection mode back to manual. This is to avoid a bad state where bulk selection mode is still enabled and more (unselected) agents are listed. - Fix the bulk selection query when some agents are excluded (managed agent policies). - Agent upgrades in bulk selection mode includes all selected agents, including agents with inactive status. - Agent policy reassign in bulk selection mode includes all selected agents, including agents with inactive status. ### Steps for testing Cf. screen recording below. #### Setup 1. Enroll a Fleet Server with a managed agent policy (e.g. by making sure the preconfigured agent policy for Fleet Server has `is_managed: true`). 2. Create agent policy "Agent policy 1". In the agent policy settings, set the inactivity timeout to a low value, e.g. 10 seconds. 3. Enroll 7 agents on agent policy "Agent policy 1" (e.g. with Horde). Once they are enrolled, kill the agents: they will become inactive in Fleet. 4. Create agent policy "Agent policy 2". Enroll 7 agents on it. #### UI 1. In the Agents table, change the filtering to include inactive status. You should see 15 agents: 7 Healthy, 7 Inactive, 1 (Healthy) Fleet Server. The Fleet Server should not be manually selectable (managed agent policy). 2. Select one inactive agent. In the Actions menu, the agent count should be 1 and actions should be available. NB: the action to schedule an upgrade requires Platinum license, so it may be disabled. 3. Manually select all agents: above the table, it should say `Showing 15 agents | 14 agents selected`. In the Actions menu, the agent count should be 14 and actions should be available. 4. Change the number of rows per page to 5; select all agents on the first page and then click `Select everything on all pages` (bulk selection): above the table, it should say `Showing 15 agents | All agents selected`. In the Actions menu, the agent count should be 14 and actions should be available. 5. Go to page 2, where 2 Healthy and 3 Inactive agents should be listed. Bulk select all agents again. Change the filtering to exclude inactive status: there should be 3 remaining agents (2 Healthy and Fleet Server) and Fleet Server should not be selected. Above the table, it should say `Showing 8 agents | 2 agents selected`. In the Actions menu, the agent count should be 2 and actions should be available. 6. Change the filtering to include inactive status again: you should see 2 selected Healthy agents and 3 unselected Inactive agents. Above the table, it should say `Showing 15 agents | 2 agents selected`. In the Actions menu, the agent count should be 2 and actions should be available. #### Bulk agent actions 1. Bulk select all 14 agents (7 Healthy, 7 Inactive) and, in the Actions menu, click "Upgrade 14 agents". The upgrade should be kicked off for all agents. In the Agents Activity flyout, you should be able to follow the upgrades for the 14 agents. 2. Create a new agent policy "Agent policy 3". Bulk select all 14 agents (7 Healthy, 7 Inactive). In the Actions menu, click "Assign to new policy" and select "Agent policy 3". All 14 agents should be reassigned to the new policy (NB: Inactive agents will get Offline status). 3. Bulk select all 14 agents (7 Healthy, 7 Inactive) and, in the Actions menu, click "Unenroll 14 agents". All agents should be unrenrolled. ### Screen recording The following recording shows the main UI fixes: - Bulk selection with inactive agents gets correct agent count - Changing the filtering in bulk selection mode changes to manual mode - Managed policy agent cannot be selected https://github.com/elastic/kibana/assets/23701614/e52b225c-2951-4729-8903-551fcc793068 ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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 --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../fleet/common/types/rest_spec/agent.ts | 3 + .../components/bulk_actions.test.tsx | 128 ++++-------------- .../components/bulk_actions.tsx | 39 ++---- .../components/search_and_filter_bar.test.tsx | 15 +- .../components/search_and_filter_bar.tsx | 20 +-- .../hooks/use_fetch_agents_data.tsx | 17 +-- .../agent_list_page/hooks/use_update_tags.tsx | 8 +- .../sections/agents/agent_list_page/index.tsx | 84 +++++++----- .../utils/get_common_tags.test.ts | 8 +- .../agent_list_page/utils/get_common_tags.ts | 8 +- .../agent_reassign_policy_modal/index.tsx | 1 + .../components/agent_upgrade_modal/index.tsx | 1 + .../fleet/server/routes/agent/handlers.ts | 6 +- .../fleet/server/routes/agent/index.ts | 4 +- .../server/routes/agent/upgrade_handler.ts | 4 +- .../fleet/server/types/rest_spec/agent.ts | 3 + 16 files changed, 133 insertions(+), 216 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index edb3f7f0eefcb..07553dbc21acf 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -114,6 +114,7 @@ export interface PostBulkAgentUpgradeRequest { rollout_duration_seconds?: number; start_time?: string; force?: boolean; + includeInactive?: boolean; }; } @@ -147,6 +148,7 @@ export interface PostBulkAgentReassignRequest { policy_id: string; agents: string[] | string; batchSize?: number; + includeInactive?: boolean; }; } @@ -185,6 +187,7 @@ export interface PostBulkUpdateAgentTagsRequest { agents: string[] | string; tagsToAdd?: string[]; tagsToRemove?: string[]; + includeInactive?: boolean; }; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.test.tsx index 5834c776a6dce..f9ec74f7ce123 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.test.tsx @@ -12,29 +12,29 @@ import { fireEvent, act } from '@testing-library/react'; import type { Agent } from '../../../../types'; import { createFleetTestRendererMock } from '../../../../../../mock'; +import type { LicenseService } from '../../../../services'; import { ExperimentalFeaturesService } from '../../../../services'; import { AgentReassignAgentPolicyModal } from '../../components/agent_reassign_policy_modal'; +import { useLicense } from '../../../../../../hooks/use_license'; + import { AgentBulkActions } from './bulk_actions'; jest.mock('../../../../../../services/experimental_features'); const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService); -jest.mock('../../../../hooks', () => ({ - ...jest.requireActual('../../../../hooks'), -})); +jest.mock('../../../../../../hooks/use_license'); +const mockedUseLicence = useLicense as jest.MockedFunction; jest.mock('../../components/agent_reassign_policy_modal'); const defaultProps = { - shownAgents: 10, - inactiveShownAgents: 0, + nAgentsInTable: 10, totalManagedAgentIds: [], - inactiveManagedAgentIds: [], selectionMode: 'manual', currentQuery: '', selectedAgents: [], - visibleAgents: [], + agentsOnCurrentPage: [], refreshAgents: () => undefined, allTags: [], agentPolicies: [], @@ -43,50 +43,28 @@ const defaultProps = { describe('AgentBulkActions', () => { beforeAll(() => { mockedExperimentalFeaturesService.get.mockReturnValue({ - diagnosticFileUploadEnabled: false, + diagnosticFileUploadEnabled: true, } as any); }); beforeEach(() => { + mockedUseLicence.mockReturnValue({ + hasAtLeast: () => false, + } as unknown as LicenseService); jest.mocked(AgentReassignAgentPolicyModal).mockReset(); jest.mocked(AgentReassignAgentPolicyModal).mockReturnValue(null); }); function render(props: any) { const renderer = createFleetTestRendererMock(); - return renderer.render(); } - describe('When in manual mode', () => { - it('should show only disabled actions if no agents are active', async () => { - const results = render({ - ...defaultProps, - inactiveShownAgents: 10, - selectedAgents: [{ id: 'agent1' }, { id: 'agent2' }] as Agent[], - }); - - const bulkActionsButton = results.getByTestId('agentBulkActionsButton'); - await act(async () => { - fireEvent.click(bulkActionsButton); - }); - - expect(results.getByText('Add / remove tags').closest('button')!).toBeDisabled(); - expect(results.getByText('Assign to new policy').closest('button')!).toBeDisabled(); - expect(results.getByText('Unenroll 2 agents').closest('button')!).toBeDisabled(); - expect(results.getByText('Upgrade 2 agents').closest('button')!).toBeDisabled(); - expect(results.getByText('Schedule upgrade for 2 agents').closest('button')!).toBeDisabled(); - expect(results.queryByText('Request diagnostics for 2 agents')).toBeNull(); - expect(results.getByText('Restart upgrade 2 agents').closest('button')!).toBeDisabled(); - }); - - it('should show available actions for 2 selected agents if they are active', async () => { + describe('When in manual selection mode', () => { + it('should show the available actions for the selected agents', async () => { const results = render({ ...defaultProps, - selectedAgents: [ - { id: 'agent1', tags: ['oldTag'], active: true }, - { id: 'agent2', active: true }, - ] as Agent[], + selectedAgents: [{ id: 'agent1', tags: ['oldTag'] }, { id: 'agent2' }] as Agent[], }); const bulkActionsButton = results.getByTestId('agentBulkActionsButton'); @@ -100,19 +78,19 @@ describe('AgentBulkActions', () => { expect(results.getByText('Upgrade 2 agents').closest('button')!).toBeEnabled(); expect(results.getByText('Schedule upgrade for 2 agents').closest('button')!).toBeDisabled(); expect(results.getByText('Restart upgrade 2 agents').closest('button')!).toBeEnabled(); + expect( + results.getByText('Request diagnostics for 2 agents').closest('button')! + ).toBeEnabled(); }); - it('should add actions if mockedExperimentalFeaturesService is enabled', async () => { - mockedExperimentalFeaturesService.get.mockReturnValue({ - diagnosticFileUploadEnabled: true, - } as any); + it('should allow scheduled upgrades if the license allows it', async () => { + mockedUseLicence.mockReturnValue({ + hasAtLeast: () => true, + } as unknown as LicenseService); const results = render({ ...defaultProps, - selectedAgents: [ - { id: 'agent1', tags: ['oldTag'], active: true }, - { id: 'agent2', active: true }, - ] as Agent[], + selectedAgents: [{ id: 'agent1', tags: ['oldTag'] }, { id: 'agent2' }] as Agent[], }); const bulkActionsButton = results.getByTestId('agentBulkActionsButton'); @@ -120,18 +98,12 @@ describe('AgentBulkActions', () => { fireEvent.click(bulkActionsButton); }); - expect( - results.getByText('Request diagnostics for 2 agents').closest('button')! - ).toBeEnabled(); + expect(results.getByText('Schedule upgrade for 2 agents').closest('button')!).toBeEnabled(); }); }); - describe('When in query mode', () => { - mockedExperimentalFeaturesService.get.mockReturnValue({ - diagnosticFileUploadEnabled: true, - } as any); - - it('should show correct actions for active agents when no managed policies exist', async () => { + describe('When in query selection mode', () => { + it('should show the available actions for all agents when no managed agents are listed', async () => { const results = render({ ...defaultProps, selectionMode: 'query', @@ -153,7 +125,7 @@ describe('AgentBulkActions', () => { expect(results.getByText('Restart upgrade 10 agents').closest('button')!).toBeEnabled(); }); - it('should show correct actions for the active agents and exclude the managed agents from the count', async () => { + it('should show the available actions for all agents except managed agents', async () => { const results = render({ ...defaultProps, totalManagedAgentIds: ['agentId1', 'agentId2'], @@ -176,49 +148,7 @@ describe('AgentBulkActions', () => { expect(results.getByText('Restart upgrade 8 agents').closest('button')!).toBeEnabled(); }); - it('should show correct actions also when there are inactive managed agents', async () => { - const results = render({ - ...defaultProps, - inactiveManagedAgentIds: ['agentId1', 'agentId2'], - totalManagedAgentIds: ['agentId1', 'agentId2', 'agentId3'], - selectionMode: 'query', - }); - - const bulkActionsButton = results.getByTestId('agentBulkActionsButton'); - await act(async () => { - fireEvent.click(bulkActionsButton); - }); - - expect(results.getByText('Add / remove tags').closest('button')!).toBeEnabled(); - expect(results.getByText('Assign to new policy').closest('button')!).toBeEnabled(); - expect(results.getByText('Unenroll 9 agents').closest('button')!).toBeEnabled(); - expect(results.getByText('Upgrade 9 agents').closest('button')!).toBeEnabled(); - expect(results.getByText('Schedule upgrade for 9 agents').closest('button')!).toBeDisabled(); - expect(results.getByText('Restart upgrade 9 agents').closest('button')!).toBeEnabled(); - }); - - it('should show disabled actions when only inactive agents are selected', async () => { - const results = render({ - ...defaultProps, - inactiveShownAgents: 10, - selectedAgents: [{ id: 'agent1' }, { id: 'agent2' }] as Agent[], - selectionMode: 'query', - }); - - const bulkActionsButton = results.getByTestId('agentBulkActionsButton'); - await act(async () => { - fireEvent.click(bulkActionsButton); - }); - - expect(results.getByText('Add / remove tags').closest('button')!).toBeDisabled(); - expect(results.getByText('Assign to new policy').closest('button')!).toBeDisabled(); - expect(results.getByText('Unenroll 0 agents').closest('button')!).toBeDisabled(); - expect(results.getByText('Upgrade 0 agents').closest('button')!).toBeDisabled(); - expect(results.getByText('Schedule upgrade for 0 agents').closest('button')!).toBeDisabled(); - expect(results.getByText('Restart upgrade 0 agents').closest('button')!).toBeDisabled(); - }); - - it('should generate a correct kuery to select agents', async () => { + it('should generate a correct kuery to select agents when no managed agents are listed', async () => { const results = render({ ...defaultProps, selectionMode: 'query', @@ -243,7 +173,7 @@ describe('AgentBulkActions', () => { ); }); - it('should generate a correct kuery to select agents with managed agents too', async () => { + it('should generate a correct kuery that excludes managed agents', async () => { const results = render({ ...defaultProps, totalManagedAgentIds: ['agentId1', 'agentId2'], @@ -263,7 +193,7 @@ describe('AgentBulkActions', () => { expect(jest.mocked(AgentReassignAgentPolicyModal)).toHaveBeenCalledWith( expect.objectContaining({ - agents: '(Base query) AND NOT (fleet-agents.agent.id : ("agentId1" or "agentId2"))', + agents: '((Base query)) AND NOT (fleet-agents.agent.id : ("agentId1" or "agentId2"))', }), expect.anything() ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx index 3871473d40bae..cde9c8960e479 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/bulk_actions.tsx @@ -35,28 +35,24 @@ import type { SelectionMode } from './types'; import { TagsAddRemove } from './tags_add_remove'; export interface Props { - shownAgents: number; - inactiveShownAgents: number; + nAgentsInTable: number; totalManagedAgentIds: string[]; - inactiveManagedAgentIds: string[]; selectionMode: SelectionMode; currentQuery: string; selectedAgents: Agent[]; - visibleAgents: Agent[]; + agentsOnCurrentPage: Agent[]; refreshAgents: (args?: { refreshTags?: boolean }) => void; allTags: string[]; agentPolicies: AgentPolicy[]; } export const AgentBulkActions: React.FunctionComponent = ({ - shownAgents, - inactiveShownAgents, + nAgentsInTable, totalManagedAgentIds, - inactiveManagedAgentIds, selectionMode, currentQuery, selectedAgents, - visibleAgents, + agentsOnCurrentPage, refreshAgents, allTags, agentPolicies, @@ -87,26 +83,17 @@ export const AgentBulkActions: React.FunctionComponent = ({ const excludedKuery = `${AGENTS_PREFIX}.agent.id : (${totalManagedAgentIds .map((id) => `"${id}"`) .join(' or ')})`; - return `${currentQuery} AND NOT (${excludedKuery})`; + return `(${currentQuery}) AND NOT (${excludedKuery})`; } else { return currentQuery; } }, [currentQuery, totalManagedAgentIds]); - const totalActiveAgents = shownAgents - inactiveShownAgents; - - // exclude inactive agents from the count + const agents = selectionMode === 'manual' ? selectedAgents : selectionQuery; const agentCount = selectionMode === 'manual' ? selectedAgents.length - : totalActiveAgents - (totalManagedAgentIds?.length - inactiveManagedAgentIds?.length); - - // Check if user is working with only inactive agents - const atLeastOneActiveAgentSelected = - selectionMode === 'manual' - ? !!selectedAgents.find((agent) => agent.active) - : shownAgents > inactiveShownAgents; - const agents = selectionMode === 'manual' ? selectedAgents : selectionQuery; + : nAgentsInTable - totalManagedAgentIds?.length; const [tagsPopoverButton, setTagsPopoverButton] = useState(); const { diagnosticFileUploadEnabled } = ExperimentalFeaturesService.get(); @@ -121,7 +108,6 @@ export const AgentBulkActions: React.FunctionComponent = ({ /> ), icon: , - disabled: !atLeastOneActiveAgentSelected, onClick: (event: any) => { setTagsPopoverButton((event.target as Element).closest('button')!); setIsTagAddVisible(!isTagAddVisible); @@ -136,7 +122,6 @@ export const AgentBulkActions: React.FunctionComponent = ({ /> ), icon: , - disabled: !atLeastOneActiveAgentSelected, onClick: () => { closeMenu(); setIsReassignFlyoutOpen(true); @@ -154,7 +139,6 @@ export const AgentBulkActions: React.FunctionComponent = ({ /> ), icon: , - disabled: !atLeastOneActiveAgentSelected, onClick: () => { closeMenu(); setIsUnenrollModalOpen(true); @@ -172,7 +156,6 @@ export const AgentBulkActions: React.FunctionComponent = ({ /> ), icon: , - disabled: !atLeastOneActiveAgentSelected, onClick: () => { closeMenu(); setUpgradeModalState({ isOpen: true, isScheduled: false, isUpdating: false }); @@ -190,7 +173,7 @@ export const AgentBulkActions: React.FunctionComponent = ({ /> ), icon: , - disabled: !atLeastOneActiveAgentSelected || !isLicenceAllowingScheduleUpgrade, + disabled: !isLicenceAllowingScheduleUpgrade, onClick: () => { closeMenu(); setUpgradeModalState({ isOpen: true, isScheduled: true, isUpdating: false }); @@ -210,7 +193,6 @@ export const AgentBulkActions: React.FunctionComponent = ({ /> ), icon: , - disabled: !atLeastOneActiveAgentSelected, onClick: () => { closeMenu(); setUpgradeModalState({ isOpen: true, isScheduled: false, isUpdating: true }); @@ -230,7 +212,6 @@ export const AgentBulkActions: React.FunctionComponent = ({ /> ), icon: , - disabled: !atLeastOneActiveAgentSelected, onClick: () => { closeMenu(); setIsRequestDiagnosticsModalOpen(true); @@ -246,8 +227,8 @@ export const AgentBulkActions: React.FunctionComponent = ({ ]; const getSelectedTagsFromAgents = useMemo( - () => getCommonTags(agents, visibleAgents ?? [], agentPolicies), - [agents, visibleAgents, agentPolicies] + () => getCommonTags(agents, agentsOnCurrentPage ?? [], agentPolicies), + [agents, agentsOnCurrentPage, agentPolicies] ); return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx index 07ba1a3402403..e7229199995ee 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.test.tsx @@ -45,15 +45,14 @@ describe('SearchAndFilterBar', () => { it('should show no Actions button when no agent is selected', async () => { const selectedAgents: Agent[] = []; const props: any = { - shownAgents: 10, - inactiveShownAgents: 0, + nAgentsInTable: 10, totalInactiveAgents: 2, totalManagedAgentIds: [], selectionMode: 'manual', currentQuery: '', selectedAgents, refreshAgents: () => undefined, - visibleAgents: [], + agentsOnCurrentPage: [], tags: [], agentPolicies: [], selectedStatus: [], @@ -79,15 +78,14 @@ describe('SearchAndFilterBar', () => { }, ]; const props: any = { - shownAgents: 10, - inactiveShownAgents: 0, + nAgentsInTable: 10, totalInactiveAgents: 2, totalManagedAgentIds: [], selectionMode: 'manual', currentQuery: '', selectedAgents, refreshAgents: () => undefined, - visibleAgents: [], + agentsOnCurrentPage: [], tags: [], agentPolicies: [], selectedStatus: [], @@ -101,15 +99,14 @@ describe('SearchAndFilterBar', () => { it('should show an Actions button when agents selected in query mode', async () => { const props: any = { - shownAgents: 10, - inactiveShownAgents: 0, + nAgentsInTable: 10, totalInactiveAgents: 2, totalManagedAgentIds: [], selectionMode: 'query', currentQuery: '', selectedAgents: [], refreshAgents: () => undefined, - visibleAgents: [], + agentsOnCurrentPage: [], tags: [], agentPolicies: [], selectedStatus: [], 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 3dab659e64232..1fd8d0930e9f3 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 @@ -46,18 +46,16 @@ export interface SearchAndFilterBarProps { tags: string[]; selectedTags: string[]; onSelectedTagsChange: (selectedTags: string[]) => void; - shownAgents: number; - inactiveShownAgents: number; + nAgentsInTable: number; totalInactiveAgents: number; totalManagedAgentIds: string[]; - inactiveManagedAgentIds: string[]; selectionMode: SelectionMode; currentQuery: string; selectedAgents: Agent[]; refreshAgents: (args?: { refreshTags?: boolean }) => void; onClickAddAgent: () => void; onClickAddFleetServer: () => void; - visibleAgents: Agent[]; + agentsOnCurrentPage: Agent[]; onClickAgentActivity: () => void; showAgentActivityTour: { isOpen: boolean }; } @@ -76,18 +74,16 @@ export const SearchAndFilterBar: React.FunctionComponent { @@ -198,17 +194,15 @@ export const SearchAndFilterBar: React.FunctionComponent {(selectionMode === 'manual' && selectedAgents.length) || - (selectionMode === 'query' && shownAgents > 0) ? ( + (selectionMode === 'query' && nAgentsInTable > 0) ? ( (); const [allTags, setAllTags] = useState(); const [isLoading, setIsLoading] = useState(false); - const [shownAgents, setShownAgents] = useState(0); - const [inactiveShownAgents, setInactiveShownAgents] = useState(0); + const [nAgentsInTable, setNAgentsInTable] = useState(0); const [totalInactiveAgents, setTotalInactiveAgents] = useState(0); const [totalManagedAgentIds, setTotalManagedAgentIds] = useState([]); - const [inactiveManagedAgentIds, setinactiveManagedAgentIds] = useState([]); const [managedAgentsOnCurrentPage, setManagedAgentsOnCurrentPage] = useState(0); const getSortFieldForAPI = (field: keyof Agent): string => { @@ -201,11 +199,8 @@ export function useFetchAgentsData() { } setAgentsOnCurrentPage(agentsResponse.data.items); - setShownAgents(agentsResponse.data.total); + setNAgentsInTable(agentsResponse.data.total); setTotalInactiveAgents(totalInactiveAgentsResponse.data.results.inactive || 0); - setInactiveShownAgents( - showInactive ? totalInactiveAgentsResponse.data.results.inactive || 0 : 0 - ); const managedAgentPolicies = managedAgentPoliciesResponse.data?.items ?? []; @@ -227,11 +222,7 @@ export function useFetchAgentsData() { } const allManagedAgents = response.data?.items ?? []; const allManagedAgentIds = allManagedAgents?.map((agent) => agent.id); - const inactiveManagedIds = allManagedAgents - ?.filter((agent) => agent.status === 'inactive') - .map((agent) => agent.id); setTotalManagedAgentIds(allManagedAgentIds); - setinactiveManagedAgentIds(inactiveManagedIds); setManagedAgentsOnCurrentPage( agentsResponse.data.items @@ -298,11 +289,9 @@ export function useFetchAgentsData() { agentsOnCurrentPage, agentsStatus, isLoading, - shownAgents, - inactiveShownAgents, + nAgentsInTable, totalInactiveAgents, totalManagedAgentIds, - inactiveManagedAgentIds, managedAgentsOnCurrentPage, showUpgradeable, setShowUpgradeable, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.tsx index 96e619db12f09..5b3c3334be0fd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_update_tags.tsx @@ -79,7 +79,13 @@ export const useUpdateTags = () => { errorMessage?: string ) => { await wrapRequest( - async () => await sendPostBulkAgentTagsUpdate({ agents, tagsToAdd, tagsToRemove }), + async () => + await sendPostBulkAgentTagsUpdate({ + agents, + tagsToAdd, + tagsToRemove, + includeInactive: true, + }), onSuccess, successMessage, errorMessage diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index afa26547fecc7..6af126747bd99 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -49,16 +49,39 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const [selectedAgents, setSelectedAgents] = useState([]); const [selectionMode, setSelectionMode] = useState('manual'); + // Agent enrollment flyout state + const [enrollmentFlyout, setEnrollmentFlyoutState] = useState<{ + isOpen: boolean; + selectedPolicyId?: string; + }>({ + isOpen: false, + }); + const [isAgentActivityFlyoutOpen, setAgentActivityFlyoutOpen] = useState(false); + const flyoutContext = useFlyoutContext(); + + // Agent actions states + const [agentToReassign, setAgentToReassign] = useState(undefined); + const [agentToUnenroll, setAgentToUnenroll] = useState(undefined); + const [agentToGetUninstallCommand, setAgentToGetUninstallCommand] = useState( + undefined + ); + const [agentToUpgrade, setAgentToUpgrade] = useState(undefined); + const [agentToAddRemoveTags, setAgentToAddRemoveTags] = useState(undefined); + const [tagsPopoverButton, setTagsPopoverButton] = useState(); + const [showTagsAddRemove, setShowTagsAddRemove] = useState(false); + const [agentToRequestDiagnostics, setAgentToRequestDiagnostics] = useState( + undefined + ); + const [showAgentActivityTour, setShowAgentActivityTour] = useState({ isOpen: false }); + const { allTags, agentsOnCurrentPage, agentsStatus, isLoading, - shownAgents, - inactiveShownAgents, + nAgentsInTable, totalInactiveAgents, totalManagedAgentIds, - inactiveManagedAgentIds, managedAgentsOnCurrentPage, showUpgradeable, setShowUpgradeable, @@ -122,31 +145,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { setShowUpgradeable, ]); - // Agent enrollment flyout state - const [enrollmentFlyout, setEnrollmentFlyoutState] = useState<{ - isOpen: boolean; - selectedPolicyId?: string; - }>({ - isOpen: false, - }); - const [isAgentActivityFlyoutOpen, setAgentActivityFlyoutOpen] = useState(false); - const flyoutContext = useFlyoutContext(); - - // Agent actions states - const [agentToReassign, setAgentToReassign] = useState(undefined); - const [agentToUnenroll, setAgentToUnenroll] = useState(undefined); - const [agentToGetUninstallCommand, setAgentToGetUninstallCommand] = useState( - undefined - ); - const [agentToUpgrade, setAgentToUpgrade] = useState(undefined); - const [agentToAddRemoveTags, setAgentToAddRemoveTags] = useState(undefined); - const [tagsPopoverButton, setTagsPopoverButton] = useState(); - const [showTagsAddRemove, setShowTagsAddRemove] = useState(false); - const [agentToRequestDiagnostics, setAgentToRequestDiagnostics] = useState( - undefined - ); - const [showAgentActivityTour, setShowAgentActivityTour] = useState({ isOpen: false }); - const onTableChange = ({ page, sort, @@ -213,7 +211,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { differenceBy(selectedAgents, agentsOnCurrentPage, 'id').length === 0; if (!areSelectedAgentsStillVisible) { // force selecting all agents on current page if staying in query mode - return setSelectedAgents(agentsOnCurrentPage); + return setSelectedAgents(agentsOnCurrentPage.filter((agent) => isAgentSelectable(agent))); } else { setSelectionMode('manual'); } @@ -221,6 +219,20 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { setSelectedAgents(newAgents); }; + const onSelectedStatusChange = (status: string[]) => { + if (selectionMode === 'query') { + setSelectionMode('manual'); + } + setSelectedStatus(status); + }; + + const onSelectedAgentPoliciesChange = (policies: string[]) => { + if (selectionMode === 'query') { + setSelectionMode('manual'); + } + setSelectedAgentPolicies(policies); + }; + const agentToUnenrollHasFleetServer = useMemo(() => { if (!agentToUnenroll || !agentToUnenroll.policy_id) { return false; @@ -390,26 +402,24 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { onDraftKueryChange={setDraftKuery} onSubmitSearch={onSubmitSearch} selectedAgentPolicies={selectedAgentPolicies} - onSelectedAgentPoliciesChange={setSelectedAgentPolicies} + onSelectedAgentPoliciesChange={onSelectedAgentPoliciesChange} selectedStatus={selectedStatus} - onSelectedStatusChange={setSelectedStatus} + onSelectedStatusChange={onSelectedStatusChange} showUpgradeable={showUpgradeable} onShowUpgradeableChange={setShowUpgradeable} tags={allTags ?? []} selectedTags={selectedTags} onSelectedTagsChange={setSelectedTags} - shownAgents={shownAgents} - inactiveShownAgents={inactiveShownAgents} + nAgentsInTable={nAgentsInTable} totalInactiveAgents={totalInactiveAgents} totalManagedAgentIds={totalManagedAgentIds} - inactiveManagedAgentIds={inactiveManagedAgentIds} selectionMode={selectionMode} currentQuery={kuery} selectedAgents={selectedAgents} refreshAgents={refreshAgents} onClickAddAgent={() => setEnrollmentFlyoutState({ isOpen: true })} onClickAddFleetServer={onClickAddFleetServer} - visibleAgents={agentsOnCurrentPage} + agentsOnCurrentPage={agentsOnCurrentPage} onClickAgentActivity={onClickAgentActivity} showAgentActivityTour={showAgentActivityTour} /> @@ -417,7 +427,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { {/* Agent total, bulk actions and status bar */} = () => { showUpgradeable={showUpgradeable} onTableChange={onTableChange} pagination={pagination} - totalAgents={Math.min(shownAgents, SO_SEARCH_LIMIT)} + totalAgents={Math.min(nAgentsInTable, SO_SEARCH_LIMIT)} isUsingFilter={isUsingFilter} setEnrollmentFlyoutState={setEnrollmentFlyoutState} clearFilters={clearFilters} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/get_common_tags.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/get_common_tags.test.ts index 9e2911ead130e..82c5c2fd2cf1a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/get_common_tags.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/get_common_tags.test.ts @@ -10,7 +10,7 @@ import type { Agent, AgentPolicy } from '../../../../types'; import { getCommonTags } from './get_common_tags'; describe('getCommonTags', () => { - it('should return common tags from visibleAgents if agents is empty string', () => { + it('should return common tags from agentsOnCurrentPage if agents is empty string', () => { const result = getCommonTags( '', [{ tags: ['tag1'] }, { tags: ['tag1', 'tag2'] }] as Agent[], @@ -20,7 +20,7 @@ describe('getCommonTags', () => { expect(result).toEqual(['tag1']); }); - it('should return common tags from visibleAgents if agents is query', () => { + it('should return common tags from agentsOnCurrentPage if agents is query', () => { const result = getCommonTags( 'query', [{ tags: ['tag1'] }, { tags: ['tag1', 'tag2'] }] as Agent[], @@ -30,7 +30,7 @@ describe('getCommonTags', () => { expect(result).toEqual(['tag1']); }); - it('should return empty common tags if visibleAgents is empty', () => { + it('should return empty common tags if agentsOnCurrentPage is empty', () => { const result = getCommonTags('', [], []); expect(result).toEqual([]); @@ -52,7 +52,7 @@ describe('getCommonTags', () => { expect(result).toEqual(['oldTag', 'tag1']); }); - it('should return common tags from old data if visibleAgents empty', () => { + it('should return common tags from old data if agentsOnCurrentPage empty', () => { const result = getCommonTags( [ { id: 'agent1', tags: ['oldTag'] }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/get_common_tags.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/get_common_tags.ts index 8b638129d2d7c..c75042b0429f9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/get_common_tags.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/get_common_tags.ts @@ -11,7 +11,7 @@ import type { Agent, AgentPolicy } from '../../../../types'; export const getCommonTags = ( agents: string | Agent[], - visibleAgents: Agent[], + agentsOnCurrentPage: Agent[], agentPolicies: AgentPolicy[] ): string[] => { const isManagedPolicy = (agent: Agent): boolean => { @@ -33,12 +33,12 @@ export const getCommonTags = ( if (!Array.isArray(agents)) { // in query mode, returning common tags of all agents in current page // this is a simplification to avoid querying all agents from backend to determine common tags - return commonSelectedTags(visibleAgents); + return commonSelectedTags(agentsOnCurrentPage); } // taking latest tags from freshly loaded agents data, as selected agents array does not contain the latest tags of agents const freshSelectedAgentsData = - visibleAgents.length > 0 - ? visibleAgents.filter((newAgent) => + agentsOnCurrentPage.length > 0 + ? agentsOnCurrentPage.filter((newAgent) => agents.find((existingAgent) => existingAgent.id === newAgent.id) ) : agents; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx index 5fd6183863fb6..189f6d4455f8d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_reassign_policy_modal/index.tsx @@ -77,6 +77,7 @@ export const AgentReassignAgentPolicyModal: React.FunctionComponent = ({ : await sendPostBulkAgentReassign({ policy_id: selectedAgentPolicyId, agents: Array.isArray(agents) ? agents.map((agent) => agent.id) : agents, + includeInactive: true, }); if (res.error) { throw res.error; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx index 9b3af9f1c29ea..74bd3fe35d308 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx @@ -318,6 +318,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent @@ -283,7 +283,7 @@ export const postBulkAgentsReassignHandler: RequestHandler< const esClient = coreContext.elasticsearch.client.asInternalUser; const agentOptions = Array.isArray(request.body.agents) ? { agentIds: request.body.agents } - : { kuery: request.body.agents }; + : { kuery: request.body.agents, showInactive: request.body.includeInactive }; try { const results = await AgentService.reassignAgents( diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index ca37cc76c3381..9625b53a0867f 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -50,7 +50,7 @@ import { deleteAgentHandler, getAgentStatusForAgentPolicyHandler, putAgentsReassignHandlerDeprecated, - postBulkAgentsReassignHandler, + postBulkAgentReassignHandler, getAgentDataHandler, bulkUpdateAgentTagsHandler, getAvailableVersionsHandler, @@ -440,7 +440,7 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT version: API_VERSIONS.public.v1, validate: { request: PostBulkAgentReassignRequestSchema }, }, - postBulkAgentsReassignHandler + postBulkAgentReassignHandler ); // Bulk unenroll diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index f46cdc96cd7cb..4e4e8d1f367ca 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -172,7 +172,9 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< } try { - const agentOptions = Array.isArray(agents) ? { agentIds: agents } : { kuery: agents }; + const agentOptions = Array.isArray(agents) + ? { agentIds: agents } + : { kuery: agents, showInactive: request.body.includeInactive }; const upgradeOptions = { ...agentOptions, sourceUri, diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 882530f372f87..456463561f0b2 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -137,6 +137,7 @@ export const PostBulkAgentUpgradeRequestSchema = { }) ), batchSize: schema.maybe(schema.number()), + includeInactive: schema.boolean({ defaultValue: false }), }), }; @@ -189,6 +190,7 @@ export const PostBulkAgentReassignRequestSchema = { policy_id: schema.string(), agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), batchSize: schema.maybe(schema.number()), + includeInactive: schema.boolean({ defaultValue: false }), }), }; @@ -214,6 +216,7 @@ export const PostBulkUpdateAgentTagsRequestSchema = { tagsToAdd: schema.maybe(schema.arrayOf(schema.string())), tagsToRemove: schema.maybe(schema.arrayOf(schema.string())), batchSize: schema.maybe(schema.number()), + includeInactive: schema.boolean({ defaultValue: false }), }), };