From 62660b1ff4e98a5de50c3f188ff8fe5265dfd8b6 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Mon, 29 Jan 2024 14:34:03 +0100 Subject: [PATCH] [Fleet] Extract hook for agentList table logic (#175528) Closes https://github.com/elastic/kibana/issues/131153 Follow up of https://github.com/elastic/kibana/pull/175318 ## Summary Extract the main logic for agentList component in a separate hook. ### Checklist - [ ] [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 --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../agents/agent_list_page/hooks/index.tsx | 1 + .../hooks/use_fetch_agents_data.test.tsx | 142 ++++++++ .../hooks/use_fetch_agents_data.tsx | 314 +++++++++++++++++ .../sections/agents/agent_list_page/index.tsx | 315 +++--------------- 4 files changed, 512 insertions(+), 260 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/index.tsx index 29d4b24443031..b61dcdf88b964 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/index.tsx @@ -11,3 +11,4 @@ export { useLastSeenInactiveAgentsCount } from './use_last_seen_inactive_agents_ export { useInactiveAgentsCalloutHasBeenDismissed } from './use_inactive_agents_callout_has_been_dismissed'; export { useAgentSoftLimit } from './use_agent_soft_limit'; export { useMissingEncryptionKeyCallout } from './use_missing_encryption_key_callout'; +export { useFetchAgentsData } from './use_fetch_agents_data'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.test.tsx new file mode 100644 index 0000000000000..6e948f45ea942 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.test.tsx @@ -0,0 +1,142 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { useStartServices } from '../../../../hooks'; + +import { ExperimentalFeaturesService } from '../../../../services'; + +import { useFetchAgentsData } from './use_fetch_agents_data'; + +jest.mock('../../../../../../services/experimental_features'); +const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService); + +jest.mock('../../../../hooks', () => ({ + ...jest.requireActual('../../../../hooks'), + sendGetAgents: jest.fn().mockResolvedValue({ + data: { + total: 5, + }, + }), + sendGetAgentStatus: jest.fn().mockResolvedValue({ + data: { + results: { + inactive: 2, + }, + totalInactive: 2, + }, + }), + sendGetAgentPolicies: jest.fn().mockReturnValue({ + data: { + items: [ + { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }, + { + id: 'agent-policy-managed', + name: 'Managed Agent policy', + namespace: 'default', + managed: true, + }, + ], + }, + }), + useGetAgentPolicies: jest.fn().mockReturnValue({ + data: { + items: [ + { id: 'agent-policy-1', name: 'Agent policy 1', namespace: 'default' }, + { + id: 'agent-policy-managed', + name: 'Managed Agent policy', + namespace: 'default', + managed: true, + }, + ], + }, + error: undefined, + isLoading: false, + resendRequest: jest.fn(), + } as any), + sendGetAgentTags: jest.fn().mockReturnValue({ data: { items: ['tag1', 'tag2'] } }), + useStartServices: jest.fn().mockReturnValue({ + notifications: { + toasts: { + addError: jest.fn(), + }, + }, + cloud: {}, + data: { dataViews: { getFieldsForWildcard: jest.fn() } }, + }), + usePagination: jest.fn().mockReturnValue({ + pagination: { + currentPage: 1, + pageSize: 5, + }, + pageSizeOptions: [5, 20, 50], + setPagination: jest.fn(), + }), + useUrlParams: jest.fn().mockReturnValue({ urlParams: { kuery: '' } }), +})); + +describe('useFetchAgentsData', () => { + const startServices = useStartServices(); + const mockErrorToast = startServices.notifications.toasts.addError as jest.Mock; + + beforeAll(() => { + mockedExperimentalFeaturesService.get.mockReturnValue({ + displayAgentMetrics: true, + } as any); + }); + + beforeEach(() => { + mockErrorToast.mockReset(); + mockErrorToast.mockResolvedValue({}); + }); + + it('should fetch agents and agent policies data', async () => { + let result: any | undefined; + let waitForNextUpdate: any | undefined; + await act(async () => { + ({ result, waitForNextUpdate } = renderHook(() => useFetchAgentsData())); + await waitForNextUpdate(); + }); + + expect(result?.current.selectedStatus).toEqual(['healthy', 'unhealthy', 'updating', 'offline']); + expect(result?.current.agentPolicies).toEqual([ + { + id: 'agent-policy-1', + name: 'Agent policy 1', + namespace: 'default', + }, + { + id: 'agent-policy-managed', + managed: true, + name: 'Managed Agent policy', + namespace: 'default', + }, + ]); + + expect(result?.current.agentPoliciesIndexedById).toEqual({ + 'agent-policy-1': { + id: 'agent-policy-1', + name: 'Agent policy 1', + namespace: 'default', + }, + 'agent-policy-managed': { + id: 'agent-policy-managed', + managed: true, + name: 'Managed Agent policy', + namespace: 'default', + }, + }); + expect(result?.current.kuery).toEqual( + 'status:online or (status:error or status:degraded) or (status:updating or status:unenrolling or status:enrolling) or status:offline' + ); + expect(result?.current.currentRequestRef).toEqual({ current: 1 }); + expect(result?.current.pagination).toEqual({ currentPage: 1, pageSize: 5 }); + expect(result?.current.pageSizeOptions).toEqual([5, 20, 50]); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.tsx new file mode 100644 index 0000000000000..711a5fb91a9ba --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_fetch_agents_data.tsx @@ -0,0 +1,314 @@ +/* + * 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 { useState, useMemo, useCallback, useRef, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; + +import { agentStatusesToSummary } from '../../../../../../../common/services'; + +import type { Agent, AgentPolicy, SimplifiedAgentStatus } from '../../../../types'; +import { + usePagination, + useGetAgentPolicies, + sendGetAgents, + sendGetAgentStatus, + useUrlParams, + useStartServices, + sendGetAgentTags, + sendGetAgentPolicies, +} from '../../../../hooks'; +import { AgentStatusKueryHelper, ExperimentalFeaturesService } from '../../../../services'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../../../../constants'; + +import { getKuery } from '../utils/get_kuery'; + +const REFRESH_INTERVAL_MS = 30000; + +export function useFetchAgentsData() { + const { displayAgentMetrics } = ExperimentalFeaturesService.get(); + + const { notifications } = useStartServices(); + // useBreadcrumbs('agent_list'); + const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; + + // Agent data states + const [showUpgradeable, setShowUpgradeable] = useState(false); + + // Table and search states + const [draftKuery, setDraftKuery] = useState(defaultKuery); + const [search, setSearch] = useState(defaultKuery); + const { pagination, pageSizeOptions, setPagination } = usePagination(); + const [sortField, setSortField] = useState('enrolled_at'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + + const VERSION_FIELD = 'local_metadata.elastic.agent.version'; + const HOSTNAME_FIELD = 'local_metadata.host.hostname'; + + // Policies state for filtering + const [selectedAgentPolicies, setSelectedAgentPolicies] = useState([]); + + // Status for filtering + const [selectedStatus, setSelectedStatus] = useState([ + 'healthy', + 'unhealthy', + 'updating', + 'offline', + ]); + + const [selectedTags, setSelectedTags] = useState([]); + + const showInactive = useMemo(() => { + return selectedStatus.some((status) => status === 'inactive' || status === 'unenrolled'); + }, [selectedStatus]); + + // filters kuery + const kuery = useMemo(() => { + return getKuery({ + search, + selectedAgentPolicies, + selectedTags, + selectedStatus, + }); + }, [search, selectedAgentPolicies, selectedStatus, selectedTags]); + + const [agentsOnCurrentPage, setAgentsOnCurrentPage] = useState([]); + const [agentsStatus, setAgentsStatus] = useState< + { [key in SimplifiedAgentStatus]: number } | undefined + >(); + const [allTags, setAllTags] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [shownAgents, setShownAgents] = useState(0); + const [inactiveShownAgents, setInactiveShownAgents] = 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 => { + if ([VERSION_FIELD, HOSTNAME_FIELD].includes(field as string)) { + return `${field}.keyword`; + } + return field; + }; + + const isLoadingVar = useRef(false); + + // Request to fetch agents and agent status + const currentRequestRef = useRef(0); + const fetchData = useCallback( + ({ refreshTags = false }: { refreshTags?: boolean } = {}) => { + async function fetchDataAsync() { + // skipping refresh if previous request is in progress + if (isLoadingVar.current) { + return; + } + currentRequestRef.current++; + const currentRequest = currentRequestRef.current; + isLoadingVar.current = true; + + try { + setIsLoading(true); + const [ + agentsResponse, + totalInactiveAgentsResponse, + managedAgentPoliciesResponse, + agentTagsResponse, + ] = await Promise.all([ + sendGetAgents({ + page: pagination.currentPage, + perPage: pagination.pageSize, + kuery: kuery && kuery !== '' ? kuery : undefined, + sortField: getSortFieldForAPI(sortField), + sortOrder, + showInactive, + showUpgradeable, + getStatusSummary: true, + withMetrics: displayAgentMetrics, + }), + sendGetAgentStatus({ + kuery: AgentStatusKueryHelper.buildKueryForInactiveAgents(), + }), + sendGetAgentPolicies({ + kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.is_managed:true`, + perPage: SO_SEARCH_LIMIT, + full: false, + }), + sendGetAgentTags({ + kuery: kuery && kuery !== '' ? kuery : undefined, + showInactive, + }), + ]); + isLoadingVar.current = false; + // Return if a newer request has been triggered + if (currentRequestRef.current !== currentRequest) { + return; + } + if (agentsResponse.error) { + throw agentsResponse.error; + } + if (!agentsResponse.data) { + throw new Error('Invalid GET /agents response'); + } + if (!totalInactiveAgentsResponse.data) { + throw new Error('Invalid GET /agents_status response'); + } + if (managedAgentPoliciesResponse.error) { + throw new Error(managedAgentPoliciesResponse.error.message); + } + if (agentTagsResponse.error) { + throw agentTagsResponse.error; + } + if (!agentTagsResponse.data) { + throw new Error('Invalid GET /agent/tags response'); + } + + const statusSummary = agentsResponse.data.statusSummary; + if (!statusSummary) { + throw new Error('Invalid GET /agents response - no status summary'); + } + setAgentsStatus(agentStatusesToSummary(statusSummary)); + + const newAllTags = agentTagsResponse.data.items; + // We only want to update the list of available tags if + // - We haven't set any tags yet + // - We've received the "refreshTags" flag which will force a refresh of the tags list when an agent is unenrolled + // - Tags are modified (add, remove, edit) + if (!allTags || refreshTags || !isEqual(newAllTags, allTags)) { + setAllTags(newAllTags); + } + + setAgentsOnCurrentPage(agentsResponse.data.items); + setShownAgents(agentsResponse.data.total); + setTotalInactiveAgents(totalInactiveAgentsResponse.data.results.inactive || 0); + setInactiveShownAgents( + showInactive ? totalInactiveAgentsResponse.data.results.inactive || 0 : 0 + ); + + const managedAgentPolicies = managedAgentPoliciesResponse.data?.items ?? []; + + if (managedAgentPolicies.length === 0) { + setTotalManagedAgentIds([]); + setManagedAgentsOnCurrentPage(0); + } else { + // Find all the agents that have managed policies and are not unenrolled + const policiesKuery = managedAgentPolicies + .map((policy) => `policy_id:"${policy.id}"`) + .join(' or '); + const response = await sendGetAgents({ + kuery: `NOT (status:unenrolled) and ${policiesKuery}`, + perPage: SO_SEARCH_LIMIT, + showInactive: true, + }); + if (response.error) { + throw new Error(response.error.message); + } + 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 + .map((agent) => agent.id) + .filter((agentId) => allManagedAgentIds.includes(agentId)).length + ); + } + } catch (error) { + notifications.toasts.addError(error, { + title: i18n.translate('xpack.fleet.agentList.errorFetchingDataTitle', { + defaultMessage: 'Error fetching agents', + }), + }); + } + setIsLoading(false); + } + fetchDataAsync(); + }, + [ + pagination.currentPage, + pagination.pageSize, + kuery, + sortField, + sortOrder, + showInactive, + showUpgradeable, + displayAgentMetrics, + allTags, + notifications.toasts, + ] + ); + + // Send request to get agent list and status + useEffect(() => { + fetchData(); + const interval = setInterval(() => { + fetchData(); + }, REFRESH_INTERVAL_MS); + + return () => clearInterval(interval); + }, [fetchData]); + + const agentPoliciesRequest = useGetAgentPolicies({ + page: 1, + perPage: SO_SEARCH_LIMIT, + full: true, + }); + + const agentPolicies = useMemo( + () => (agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []), + [agentPoliciesRequest] + ); + const agentPoliciesIndexedById = useMemo(() => { + return agentPolicies.reduce((acc, agentPolicy) => { + acc[agentPolicy.id] = agentPolicy; + + return acc; + }, {} as { [k: string]: AgentPolicy }); + }, [agentPolicies]); + + return { + allTags, + setAllTags, + agentsOnCurrentPage, + agentsStatus, + isLoading, + shownAgents, + inactiveShownAgents, + totalInactiveAgents, + totalManagedAgentIds, + inactiveManagedAgentIds, + managedAgentsOnCurrentPage, + showUpgradeable, + setShowUpgradeable, + search, + setSearch, + selectedAgentPolicies, + setSelectedAgentPolicies, + sortField, + setSortField, + sortOrder, + setSortOrder, + selectedStatus, + setSelectedStatus, + selectedTags, + setSelectedTags, + agentPolicies, + agentPoliciesRequest, + agentPoliciesIndexedById, + pagination, + pageSizeOptions, + setPagination, + kuery, + draftKuery, + setDraftKuery, + fetchData, + currentRequestRef, + }; +} 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 53e78cacfe110..af91e65a40313 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 @@ -4,35 +4,22 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react'; +import React, { useState, useMemo, useCallback, useRef } from 'react'; import { differenceBy, isEqual } from 'lodash'; import type { EuiBasicTable } from '@elastic/eui'; import { EuiSpacer, EuiPortal } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { agentStatusesToSummary } from '../../../../../../common/services'; +import type { Agent } from '../../../types'; -import type { Agent, AgentPolicy, SimplifiedAgentStatus } from '../../../types'; import { - usePagination, - useGetAgentPolicies, - sendGetAgents, - sendGetAgentStatus, - useUrlParams, useBreadcrumbs, useStartServices, useFlyoutContext, - sendGetAgentTags, useFleetServerStandalone, - sendGetAgentPolicies, } from '../../../hooks'; import { AgentEnrollmentFlyout, UninstallCommandFlyout } from '../../../components'; -import { - AgentStatusKueryHelper, - ExperimentalFeaturesService, - policyHasFleetServer, -} from '../../../services'; -import { AGENT_POLICY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../../../constants'; +import { policyHasFleetServer } from '../../../services'; +import { SO_SEARCH_LIMIT } from '../../../constants'; import { AgentReassignAgentPolicyModal, AgentUnenrollAgentModal, @@ -45,40 +32,62 @@ import { useFleetServerUnhealthy } from '../hooks/use_fleet_server_unhealthy'; import { AgentRequestDiagnosticsModal } from '../components/agent_request_diagnostics_modal'; -import { AgentTableHeader } from './components/table_header'; import type { SelectionMode } from './components/types'; + +import { AgentTableHeader } from './components/table_header'; import { SearchAndFilterBar } from './components/search_and_filter_bar'; import { TagsAddRemove } from './components/tags_add_remove'; import { AgentActivityFlyout, AgentSoftLimitCallout } from './components'; import { TableRowActions } from './components/table_row_actions'; import { AgentListTable } from './components/agent_list_table'; -import { getKuery } from './utils/get_kuery'; -import { useAgentSoftLimit, useMissingEncryptionKeyCallout } from './hooks'; - -const REFRESH_INTERVAL_MS = 30000; +import { useAgentSoftLimit, useMissingEncryptionKeyCallout, useFetchAgentsData } from './hooks'; export const AgentListPage: React.FunctionComponent<{}> = () => { - const { displayAgentMetrics } = ExperimentalFeaturesService.get(); - - const { notifications, cloud } = useStartServices(); + const { cloud } = useStartServices(); useBreadcrumbs('agent_list'); - const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; - - // Agent data states - const [showUpgradeable, setShowUpgradeable] = useState(false); // Table and search states - const [draftKuery, setDraftKuery] = useState(defaultKuery); - const [search, setSearch] = useState(defaultKuery); - const [selectionMode, setSelectionMode] = useState('manual'); const [selectedAgents, setSelectedAgents] = useState([]); + const [selectionMode, setSelectionMode] = useState('manual'); const tableRef = useRef>(null); - const { pagination, pageSizeOptions, setPagination } = usePagination(); - const [sortField, setSortField] = useState('enrolled_at'); - const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); - const VERSION_FIELD = 'local_metadata.elastic.agent.version'; - const HOSTNAME_FIELD = 'local_metadata.host.hostname'; + const { + allTags, + agentsOnCurrentPage, + agentsStatus, + isLoading, + shownAgents, + inactiveShownAgents, + totalInactiveAgents, + totalManagedAgentIds, + inactiveManagedAgentIds, + managedAgentsOnCurrentPage, + showUpgradeable, + setShowUpgradeable, + search, + setSearch, + selectedAgentPolicies, + setSelectedAgentPolicies, + sortField, + setSortField, + sortOrder, + setSortOrder, + selectedStatus, + setSelectedStatus, + selectedTags, + setSelectedTags, + agentPolicies, + agentPoliciesRequest, + agentPoliciesIndexedById, + pagination, + pageSizeOptions, + setPagination, + kuery, + draftKuery, + setDraftKuery, + fetchData, + currentRequestRef, + } = useFetchAgentsData(); const onSubmitSearch = useCallback( (newKuery: string) => { @@ -91,19 +100,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { [setSearch, pagination, setPagination] ); - // Policies state for filtering - const [selectedAgentPolicies, setSelectedAgentPolicies] = useState([]); - - // Status for filtering - const [selectedStatus, setSelectedStatus] = useState([ - 'healthy', - 'unhealthy', - 'updating', - 'offline', - ]); - - const [selectedTags, setSelectedTags] = useState([]); - const isUsingFilter = !!( search.trim() || selectedAgentPolicies.length || @@ -119,7 +115,14 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { setSelectedStatus([]); setSelectedTags([]); setShowUpgradeable(false); - }, [setSearch, setDraftKuery, setSelectedAgentPolicies, setSelectedStatus, setShowUpgradeable]); + }, [ + setDraftKuery, + setSearch, + setSelectedAgentPolicies, + setSelectedStatus, + setSelectedTags, + setShowUpgradeable, + ]); // Agent enrollment flyout state const [enrollmentFlyout, setEnrollmentFlyoutState] = useState<{ @@ -128,9 +131,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }>({ isOpen: false, }); - const [isAgentActivityFlyoutOpen, setAgentActivityFlyoutOpen] = useState(false); - const flyoutContext = useFlyoutContext(); // Agent actions states @@ -146,6 +147,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const [agentToRequestDiagnostics, setAgentToRequestDiagnostics] = useState( undefined ); + const [showAgentActivityTour, setShowAgentActivityTour] = useState({ isOpen: false }); const onTableChange = ({ page, @@ -168,36 +170,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { return selectedStatus.some((status) => status === 'inactive' || status === 'unenrolled'); }, [selectedStatus]); - // filters kuery - const kuery = useMemo(() => { - return getKuery({ - search, - selectedAgentPolicies, - selectedTags, - selectedStatus, - }); - }, [search, selectedAgentPolicies, selectedStatus, selectedTags]); - - const [agentsOnCurrentPage, setAgentsOnCurrentPage] = useState([]); - const [agentsStatus, setAgentsStatus] = useState< - { [key in SimplifiedAgentStatus]: number } | undefined - >(); - const [allTags, setAllTags] = useState(); - const [isLoading, setIsLoading] = useState(false); - const [shownAgents, setShownAgents] = useState(0); - const [inactiveShownAgents, setInactiveShownAgents] = useState(0); - const [totalInactiveAgents, setTotalInactiveAgents] = useState(0); - const [totalManagedAgentIds, setTotalManagedAgentIds] = useState([]); - const [inactiveManagedAgentIds, setInactiveManagedAgentIds] = useState([]); - const [managedAgentsOnCurrentPage, setManagedAgentsOnCurrentPage] = useState(0); - const [showAgentActivityTour, setShowAgentActivityTour] = useState({ isOpen: false }); - const getSortFieldForAPI = (field: keyof Agent): string => { - if ([VERSION_FIELD, HOSTNAME_FIELD].includes(field as string)) { - return `${field}.keyword`; - } - return field; - }; - const renderActions = (agent: Agent) => { const agentPolicy = typeof agent.policy_id === 'string' ? agentPoliciesIndexedById[agent.policy_id] : undefined; @@ -224,183 +196,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { ); }; - const isLoadingVar = useRef(false); - - // Request to fetch agents and agent status - const currentRequestRef = useRef(0); - const fetchData = useCallback( - ({ refreshTags = false }: { refreshTags?: boolean } = {}) => { - async function fetchDataAsync() { - // skipping refresh if previous request is in progress - if (isLoadingVar.current) { - return; - } - currentRequestRef.current++; - const currentRequest = currentRequestRef.current; - isLoadingVar.current = true; - - try { - setIsLoading(true); - const [ - agentsResponse, - totalInactiveAgentsResponse, - managedAgentPoliciesResponse, - agentTagsResponse, - ] = await Promise.all([ - sendGetAgents({ - page: pagination.currentPage, - perPage: pagination.pageSize, - kuery: kuery && kuery !== '' ? kuery : undefined, - sortField: getSortFieldForAPI(sortField), - sortOrder, - showInactive, - showUpgradeable, - getStatusSummary: true, - withMetrics: displayAgentMetrics, - }), - sendGetAgentStatus({ - kuery: AgentStatusKueryHelper.buildKueryForInactiveAgents(), - }), - sendGetAgentPolicies({ - kuery: `${AGENT_POLICY_SAVED_OBJECT_TYPE}.is_managed:true`, - perPage: SO_SEARCH_LIMIT, - full: false, - }), - sendGetAgentTags({ - kuery: kuery && kuery !== '' ? kuery : undefined, - showInactive, - }), - ]); - isLoadingVar.current = false; - // Return if a newer request has been triggered - if (currentRequestRef.current !== currentRequest) { - return; - } - if (agentsResponse.error) { - throw agentsResponse.error; - } - if (!agentsResponse.data) { - throw new Error('Invalid GET /agents response'); - } - if (!totalInactiveAgentsResponse.data) { - throw new Error('Invalid GET /agents_status response'); - } - if (managedAgentPoliciesResponse.error) { - throw new Error(managedAgentPoliciesResponse.error.message); - } - if (agentTagsResponse.error) { - throw agentTagsResponse.error; - } - if (!agentTagsResponse.data) { - throw new Error('Invalid GET /agent/tags response'); - } - - const statusSummary = agentsResponse.data.statusSummary; - if (!statusSummary) { - throw new Error('Invalid GET /agents response - no status summary'); - } - setAgentsStatus(agentStatusesToSummary(statusSummary)); - - const newAllTags = agentTagsResponse.data.items; - // We only want to update the list of available tags if - // - We haven't set any tags yet - // - We've received the "refreshTags" flag which will force a refresh of the tags list when an agent is unenrolled - // - Tags are modified (add, remove, edit) - if (!allTags || refreshTags || !isEqual(newAllTags, allTags)) { - setAllTags(newAllTags); - } - - setAgentsOnCurrentPage(agentsResponse.data.items); - setShownAgents(agentsResponse.data.total); - setTotalInactiveAgents(totalInactiveAgentsResponse.data.results.inactive || 0); - setInactiveShownAgents( - showInactive ? totalInactiveAgentsResponse.data.results.inactive || 0 : 0 - ); - - const managedAgentPolicies = managedAgentPoliciesResponse.data?.items ?? []; - if (managedAgentPolicies.length === 0) { - setTotalManagedAgentIds([]); - setManagedAgentsOnCurrentPage(0); - } else { - // Find all the agents that have managed policies and are not unenrolled - const policiesKuery = managedAgentPolicies - .map((policy) => `policy_id:"${policy.id}"`) - .join(' or '); - const response = await sendGetAgents({ - kuery: `NOT (status:unenrolled) and ${policiesKuery}`, - perPage: SO_SEARCH_LIMIT, - showInactive: true, - }); - if (response.error) { - throw new Error(response.error.message); - } - 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 - .map((agent) => agent.id) - .filter((agentId) => allManagedAgentIds.includes(agentId)).length - ); - } - } catch (error) { - notifications.toasts.addError(error, { - title: i18n.translate('xpack.fleet.agentList.errorFetchingDataTitle', { - defaultMessage: 'Error fetching agents', - }), - }); - } - setIsLoading(false); - } - fetchDataAsync(); - }, - [ - pagination.currentPage, - pagination.pageSize, - kuery, - sortField, - sortOrder, - showInactive, - showUpgradeable, - displayAgentMetrics, - allTags, - notifications.toasts, - ] - ); - - // Send request to get agent list and status - useEffect(() => { - fetchData(); - const interval = setInterval(() => { - fetchData(); - }, REFRESH_INTERVAL_MS); - - return () => clearInterval(interval); - }, [fetchData]); - - const agentPoliciesRequest = useGetAgentPolicies({ - page: 1, - perPage: SO_SEARCH_LIMIT, - full: true, - }); - - const agentPolicies = useMemo( - () => (agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []), - [agentPoliciesRequest] - ); - const agentPoliciesIndexedById = useMemo(() => { - return agentPolicies.reduce((acc, agentPolicy) => { - acc[agentPolicy.id] = agentPolicy; - - return acc; - }, {} as { [k: string]: AgentPolicy }); - }, [agentPolicies]); - const isAgentSelectable = useCallback( (agent: Agent) => { if (!agent.active) return false;