diff --git a/config/serverless.yml b/config/serverless.yml
index 897b168340cd5..02e7a7ea07fb8 100644
--- a/config/serverless.yml
+++ b/config/serverless.yml
@@ -2,9 +2,11 @@ interactiveSetup.enabled: false
newsfeed.enabled: false
xpack.security.showNavLinks: false
xpack.serverless.plugin.enabled: true
+# Fleet settings
xpack.fleet.internal.fleetServerStandalone: true
xpack.fleet.internal.disableILMPolicies: true
xpack.fleet.internal.disableProxies: true
+xpack.fleet.internal.activeAgentsSoftLimit: 25000
# Enable ZDT migration algorithm
migrations.algorithm: zdt
@@ -13,7 +15,7 @@ migrations.algorithm: zdt
# until the controller is able to spawn the migrator job/pod
migrations.zdt:
metaPickupSyncDelaySec: 5
- runOnRoles: ["ui"]
+ runOnRoles: ['ui']
# Ess plugins
xpack.securitySolutionEss.enabled: false
diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts
index 8ec34f1b91ec4..e050ecf0a3ba8 100644
--- a/test/plugin_functional/test_suites/core_plugins/rendering.ts
+++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts
@@ -221,6 +221,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled (boolean)',
'xpack.fleet.agents.enabled (boolean)',
'xpack.fleet.enableExperimental (array)',
+ 'xpack.fleet.internal.activeAgentsSoftLimit (number)',
'xpack.fleet.internal.disableProxies (boolean)',
'xpack.fleet.internal.fleetServerStandalone (boolean)',
'xpack.fleet.developer.maxAgentPoliciesWithInactivityTimeout (number)',
diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts
index 6f8b643b44a74..a35760e35ba17 100644
--- a/x-pack/plugins/fleet/common/types/index.ts
+++ b/x-pack/plugins/fleet/common/types/index.ts
@@ -49,6 +49,7 @@ export interface FleetConfigType {
disableILMPolicies: boolean;
disableProxies: boolean;
fleetServerStandalone: boolean;
+ activeAgentsSoftLimit?: number;
};
createArtifactsBulkBatchSize?: number;
}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_soft_limit_callout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_soft_limit_callout.tsx
new file mode 100644
index 0000000000000..68f986fc8c0f4
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_soft_limit_callout.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 from 'react';
+import { EuiCallOut } from '@elastic/eui';
+import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react';
+
+import { useConfig } from '../../../../hooks';
+
+export const AgentSoftLimitCallout = () => {
+ const config = useConfig();
+
+ return (
+
+ }
+ >
+ ,
+ }}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/index.tsx
index 05b481fb692b5..94b8565b83704 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/index.tsx
@@ -7,3 +7,4 @@
export { AgentActivityFlyout } from './agent_activity_flyout';
export { AgentActivityButton } from './agent_activity_button';
+export { AgentSoftLimitCallout } from './agent_soft_limit_callout';
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 a71ad1891b890..a270aadcd72e4 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
@@ -9,3 +9,4 @@ export { useUpdateTags } from './use_update_tags';
export { useActionStatus } from './use_action_status';
export { useLastSeenInactiveAgentsCount } from './use_last_seen_inactive_agents_count';
export { useInactiveAgentsCalloutHasBeenDismissed } from './use_inactive_agents_callout_has_been_dismissed';
+export { useAgentSoftLimit } from './use_agent_soft_limit';
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.test.tsx
new file mode 100644
index 0000000000000..4b9d908baa84d
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.test.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 { createFleetTestRendererMock } from '../../../../../../mock';
+import { useConfig, sendGetAgents } from '../../../../hooks';
+
+import { useAgentSoftLimit } from './use_agent_soft_limit';
+
+jest.mock('../../../../hooks');
+
+const mockedSendGetAgents = jest.mocked(sendGetAgents);
+const mockedUseConfig = jest.mocked(useConfig);
+
+describe('useAgentSoftLimit', () => {
+ beforeEach(() => {
+ mockedSendGetAgents.mockReset();
+ mockedUseConfig.mockReset();
+ });
+ it('should return shouldDisplayAgentSoftLimit:false if soft limit is not enabled in config', async () => {
+ const renderer = createFleetTestRendererMock();
+ mockedUseConfig.mockReturnValue({} as any);
+ const { result } = renderer.renderHook(() => useAgentSoftLimit());
+
+ expect(result.current.shouldDisplayAgentSoftLimit).toEqual(false);
+
+ expect(mockedSendGetAgents).not.toBeCalled();
+ });
+
+ it('should return shouldDisplayAgentSoftLimit:false if soft limit is enabled in config and there is less online agents than the limit', async () => {
+ const renderer = createFleetTestRendererMock();
+ mockedUseConfig.mockReturnValue({ internal: { activeAgentsSoftLimit: 10 } } as any);
+ mockedSendGetAgents.mockResolvedValue({
+ data: {
+ total: 5,
+ },
+ } as any);
+ const { result, waitForNextUpdate } = renderer.renderHook(() => useAgentSoftLimit());
+ await waitForNextUpdate();
+
+ expect(mockedSendGetAgents).toBeCalled();
+ expect(result.current.shouldDisplayAgentSoftLimit).toEqual(false);
+ });
+
+ it('should return shouldDisplayAgentSoftLimit:true if soft limit is enabled in config and there is more online agents than the limit', async () => {
+ const renderer = createFleetTestRendererMock();
+ mockedUseConfig.mockReturnValue({ internal: { activeAgentsSoftLimit: 10 } } as any);
+ mockedSendGetAgents.mockResolvedValue({
+ data: {
+ total: 15,
+ },
+ } as any);
+ const { result, waitForNextUpdate } = renderer.renderHook(() => useAgentSoftLimit());
+ await waitForNextUpdate();
+
+ expect(mockedSendGetAgents).toBeCalled();
+ expect(result.current.shouldDisplayAgentSoftLimit).toEqual(true);
+ });
+});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.tsx
new file mode 100644
index 0000000000000..e693838ca47bf
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/hooks/use_agent_soft_limit.tsx
@@ -0,0 +1,38 @@
+/*
+ * 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 { useQuery } from '@tanstack/react-query';
+
+import { useConfig, sendGetAgents } from '../../../../hooks';
+
+async function fetchTotalOnlineAgents() {
+ const response = await sendGetAgents({
+ kuery: 'status:online',
+ perPage: 0,
+ showInactive: false,
+ });
+
+ if (response.error) {
+ throw new Error(response.error.message);
+ }
+
+ return response.data?.total ?? 0;
+}
+
+export function useAgentSoftLimit() {
+ const config = useConfig();
+
+ const softLimit = config.internal?.activeAgentsSoftLimit;
+
+ const { data: totalAgents } = useQuery(['fetch-total-online-agents'], fetchTotalOnlineAgents, {
+ enabled: softLimit !== undefined,
+ });
+
+ return {
+ shouldDisplayAgentSoftLimit: softLimit && totalAgents ? totalAgents > softLimit : false,
+ };
+}
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 cec075b4a8f7a..bfb728c792a93 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
@@ -47,10 +47,11 @@ import { AgentTableHeader } from './components/table_header';
import type { SelectionMode } from './components/types';
import { SearchAndFilterBar } from './components/search_and_filter_bar';
import { TagsAddRemove } from './components/tags_add_remove';
-import { AgentActivityFlyout } from './components';
+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 } from './hooks';
const REFRESH_INTERVAL_MS = 30000;
@@ -396,6 +397,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
const { isFleetServerStandalone } = useFleetServerStandalone();
const showUnhealthyCallout = isFleetServerUnhealthy && !isFleetServerStandalone;
+ const { shouldDisplayAgentSoftLimit } = useAgentSoftLimit();
+
const onClickAddFleetServer = useCallback(() => {
flyoutContext.openFleetServerFlyout();
}, [flyoutContext]);
@@ -520,6 +523,12 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
>
)}
+ {shouldDisplayAgentSoftLimit && (
+ <>
+
+
+ >
+ )}
{/* TODO serverless agent soft limit */}
{showUnhealthyCallout && (
<>
diff --git a/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx b/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx
index c562510634022..708bab59f1d79 100644
--- a/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx
+++ b/x-pack/plugins/fleet/public/mock/create_test_renderer.tsx
@@ -13,6 +13,7 @@ import { render as reactRender, act } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import type { RenderHookResult } from '@testing-library/react-hooks';
import { Router } from '@kbn/shared-ux-router';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { themeServiceMock } from '@kbn/core/public/mocks';
@@ -59,6 +60,8 @@ export interface TestRenderer {
setHeaderActionMenu: Function;
}
+const queryClient = new QueryClient();
+
export const createFleetTestRendererMock = (): TestRenderer => {
const basePath = '/mock';
const extensions: UIExtensionsStorage = {};
@@ -72,7 +75,11 @@ export const createFleetTestRendererMock = (): TestRenderer => {
return (
- {children}
+
+
+ {children}
+
+
);
diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts
index 9f499f52e0713..eaee8ec0cd3be 100644
--- a/x-pack/plugins/fleet/server/config.ts
+++ b/x-pack/plugins/fleet/server/config.ts
@@ -42,6 +42,7 @@ export const config: PluginConfigDescriptor = {
internal: {
fleetServerStandalone: true,
disableProxies: true,
+ activeAgentsSoftLimit: true,
},
},
deprecations: ({ renameFromRoot, unused, unusedFromRoot }) => [
@@ -176,6 +177,11 @@ export const config: PluginConfigDescriptor = {
fleetServerStandalone: schema.boolean({
defaultValue: false,
}),
+ activeAgentsSoftLimit: schema.maybe(
+ schema.number({
+ min: 0,
+ })
+ ),
})
),
enabled: schema.boolean({ defaultValue: true }),