Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
<EuiLink onClick={() => clearFilters()}>
<FormattedMessage
id="xpack.fleet.agentList.clearFiltersLinkText"
defaultMessage="Clear filters"
defaultMessage="Reset filters"
/>
</EuiLink>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ export const SearchAndFilterBar: React.FunctionComponent<SearchAndFilterBarProps
agentPolicies={agentPolicies}
/>
<EuiFilterButton
isToggle
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upgradable filter button previously didn't have a clear "active" state

isSelected={showUpgradeable}
hasActiveFilters={showUpgradeable}
onClick={() => {
onShowUpgradeableChange(!showUpgradeable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export const AgentTableHeader: React.FunctionComponent<{
}) => {
return (
<>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexGroup justifyContent="flexStart" alignItems="center">
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="m">
<EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<AgentsSelectionStatus
totalAgents={totalAgents}
Expand All @@ -63,7 +63,7 @@ export const AgentTableHeader: React.FunctionComponent<{
<EuiLink onClick={() => clearFilters()}>
<FormattedMessage
id="xpack.fleet.agentList.header.clearFiltersLinkText"
defaultMessage="Clear filters"
defaultMessage="Reset filters"
/>
</EuiLink>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,40 @@ import { useFetchAgentsData } from './use_fetch_agents_data';
jest.mock('../../../../../../services/experimental_features');
const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService);

const defaultState = {
search: '',
selectedAgentPolicies: [],
selectedStatus: ['healthy', 'unhealthy', 'orphaned', 'updating', 'offline'],
selectedTags: [],
showUpgradeable: false,
sort: { field: 'enrolled_at', direction: 'desc' },
page: { index: 0, size: 20 },
};

jest.mock('./use_session_agent_list_state', () => {
let currentMockState = { ...defaultState };

const mockUseSessionAgentListState = jest.fn(() => {
const mockUpdateTableState = jest.fn((updates: any) => {
currentMockState = { ...currentMockState, ...updates };
});

return {
...currentMockState,
updateTableState: mockUpdateTableState,
onTableChange: jest.fn(),
clearFilters: jest.fn(),
resetToDefaults: jest.fn(),
};
});

return {
useSessionAgentListState: mockUseSessionAgentListState,
getDefaultAgentListState: jest.fn(() => defaultState),
defaultAgentListState: defaultState,
};
});

jest.mock('../../../../hooks', () => ({
...jest.requireActual('../../../../hooks'),
sendGetAgentsForRq: jest.fn().mockResolvedValue({
Expand Down Expand Up @@ -87,14 +121,6 @@ jest.mock('../../../../hooks', () => ({
cloud: {},
data: { dataViews: { getFieldsForWildcard: jest.fn() } },
}),
usePagination: jest.fn().mockReturnValue({
pagination: {
currentPage: 1,
pageSize: 5,
},
pageSizeOptions: [5, 20, 50],
setPagination: jest.fn(),
}),
}));

describe('useFetchAgentsData', () => {
Expand Down Expand Up @@ -149,7 +175,7 @@ describe('useFetchAgentsData', () => {
'status:online or (status:error or status:degraded) or status:orphaned or (status:updating or status:unenrolling or status:enrolling) or status:offline'
);

expect(result?.current.pagination).toEqual({ currentPage: 1, pageSize: 5 });
expect(result?.current.page).toEqual({ index: 0, size: 20 });
expect(result?.current.pageSizeOptions).toEqual([5, 20, 50]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import { useQuery } from '@kbn/react-query';

import { agentStatusesToSummary } from '../../../../../../../common/services';

import type { Agent, AgentPolicy } from '../../../../types';
import type { AgentPolicy } from '../../../../types';
import {
usePagination,
useGetAgentPolicies,
sendGetAgentStatus,
useUrlParams,
Expand All @@ -32,6 +31,8 @@ import { LEGACY_AGENT_POLICY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../../..

import { getKuery } from '../utils/get_kuery';

import { useSessionAgentListState, defaultAgentListState } from './use_session_agent_list_state';

const REFRESH_INTERVAL_MS = 30000;
const MAX_AGENT_ACTIONS = 100;
/** Allow to fetch full agent policy using a cache */
Expand Down Expand Up @@ -102,29 +103,91 @@ export function useFetchAgentsData() {
const { showAgentless } = useAgentlessResources();
const defaultKuery: string = (urlParams.kuery as string) || '';
const urlHasInactive = (urlParams.showInactive as string) === 'true';
const isUsingParams = defaultKuery || urlHasInactive;

// Extract state from session storage hook
const sessionState = useSessionAgentListState();
const {
search,
selectedAgentPolicies,
selectedStatus,
selectedTags,
showUpgradeable,
sort,
page,
updateTableState,
} = sessionState;

// If URL params are used, reset the table state to defaults with the param options
useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: There maybe opportunity here to separate out into its own hook the search session state, with url syncing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, I started a refactor to pull this out but ran into issues. didn't have confidence I could finish it & test thoroughly before leave 😅 so merged as-is

if (isUsingParams) {
updateTableState({
...defaultAgentListState,
search: defaultKuery,
selectedStatus: [...new Set([...selectedStatus, ...(urlHasInactive ? ['inactive'] : [])])],
});
}
// Empty array so that this only runs once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// Sync URL kuery param with session storage search to maintain shareable state
useEffect(() => {
const currentUrlKuery = (urlParams.kuery as string) || '';
// If search is empty and URL has kuery, or search differs from URL, update URL
if ((search === '' && currentUrlKuery !== '') || (search && search !== currentUrlKuery)) {
const { kuery: _, ...restParams } = urlParams;
const newParams = search === '' ? restParams : { ...restParams, kuery: search };
history.replace({
search: toUrlParams(newParams),
});
}
}, [search, urlParams, history, toUrlParams]);

// Flag to indicate if filters differ from default state
const isUsingFilter = useMemo(() => {
return (
search !== defaultAgentListState.search ||
!isEqual(selectedAgentPolicies, defaultAgentListState.selectedAgentPolicies) ||
!isEqual(selectedStatus, defaultAgentListState.selectedStatus) ||
!isEqual(selectedTags, defaultAgentListState.selectedTags) ||
showUpgradeable !== defaultAgentListState.showUpgradeable
);
}, [search, selectedAgentPolicies, selectedStatus, selectedTags, showUpgradeable]);

// Create individual setters using updateTableState
const setSearchState = useCallback(
(value: string) => updateTableState({ search: value }),
[updateTableState]
);

const setSelectedAgentPolicies = useCallback(
(value: string[]) => updateTableState({ selectedAgentPolicies: value }),
[updateTableState]
);

const setSelectedStatus = useCallback(
(value: string[]) => updateTableState({ selectedStatus: value }),
[updateTableState]
);

// Table and search states
const [showUpgradeable, setShowUpgradeable] = useState<boolean>(false);
const [draftKuery, setDraftKuery] = useState<string>(defaultKuery);
const [search, setSearchState] = useState<string>(defaultKuery);
const { pagination, pageSizeOptions, setPagination } = usePagination();
const [sortField, setSortField] = useState<keyof Agent>('enrolled_at');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');

// Policies state for filtering
const [selectedAgentPolicies, setSelectedAgentPolicies] = useState<string[]>([]);

// Status for filtering
const [selectedStatus, setSelectedStatus] = useState<string[]>([
'healthy',
'unhealthy',
'orphaned',
'updating',
'offline',
...(urlHasInactive ? ['inactive'] : []),
]);

const [selectedTags, setSelectedTags] = useState<string[]>([]);
const setSelectedTags = useCallback(
(value: string[]) => updateTableState({ selectedTags: value }),
[updateTableState]
);

const setShowUpgradeable = useCallback(
(value: boolean) => updateTableState({ showUpgradeable: value }),
[updateTableState]
);

const pageSizeOptions = [5, 20, 50];

// Sync draftKuery with session storage search
const [draftKuery, setDraftKuery] = useState<string>(search);
useEffect(() => {
setDraftKuery(search);
}, [search]);

const showInactive = useMemo(() => {
return selectedStatus.some((status) => status === 'inactive') || selectedStatus.length === 0;
Expand All @@ -142,13 +205,14 @@ export function useFetchAgentsData() {
}

if (urlParams.kuery !== newVal) {
const { kuery: _, ...restParams } = urlParams;
const newParams = newVal === '' ? restParams : { ...restParams, kuery: newVal };
history.replace({
// @ts-expect-error - kuery can't be undefined
search: toUrlParams({ ...urlParams, kuery: newVal === '' ? undefined : newVal }),
search: toUrlParams(newParams),
});
}
},
[urlParams, history, toUrlParams]
[setSearchState, urlParams, history, toUrlParams]
);

// filters kuery
Expand Down Expand Up @@ -192,7 +256,12 @@ export function useFetchAgentsData() {
}
}, [latestAgentActionErrors, actionErrors]);

const queryKeyPagination = JSON.stringify({ pagination, sortField, sortOrder });
// Use session storage state for pagination and sort
const queryKeyPagination = JSON.stringify({
pagination: { currentPage: page.index + 1, pageSize: page.size },
sortField: sort.field,
sortOrder: sort.direction,
});
const queryKeyFilters = JSON.stringify({
kuery,
showAgentless,
Expand All @@ -210,7 +279,7 @@ export function useFetchAgentsData() {
refetch,
} = useQuery({
queryKey: ['get-agents-list', queryKeyFilters, queryKeyPagination],
keepPreviousData: true, // Keep previous data to avoid flashing when going through pages coulse
keepPreviousData: true, // Keep previous data to avoid flashing when going through pages
queryFn: async () => {
try {
const [
Expand All @@ -220,11 +289,11 @@ export function useFetchAgentsData() {
agentTagsResponse,
] = await Promise.all([
sendGetAgentsForRq({
page: pagination.currentPage,
perPage: pagination.pageSize,
page: page.index + 1,
perPage: page.size,
kuery: kuery && kuery !== '' ? kuery : undefined,
sortField: getSortFieldForAPI(sortField),
sortOrder,
sortField: getSortFieldForAPI(sort.field),
sortOrder: sort.direction,
showAgentless,
showInactive,
showUpgradeable,
Expand Down Expand Up @@ -377,26 +446,25 @@ export function useFetchAgentsData() {
setSearch,
selectedAgentPolicies,
setSelectedAgentPolicies,
sortField,
setSortField,
sortOrder,
setSortOrder,
sort,
selectedStatus,
setSelectedStatus,
selectedTags,
setSelectedTags,
allAgentPolicies,
agentPoliciesRequest,
agentPoliciesIndexedById,
pagination,
page,
pageSizeOptions,
setPagination,
kuery,
draftKuery,
setDraftKuery,
fetchData,
queryHasChanged,
latestAgentActionErrors,
setLatestAgentActionErrors,
isUsingFilter,
clearFilters: sessionState.clearFilters,
onTableChange: sessionState.onTableChange,
};
}
Loading
Loading