Skip to content

Commit 16a0eb3

Browse files
committed
[Fleet] added modal to manage auto upgrade agents (elastic#206955)
Related to elastic/ingest-dev#4721 Added Agent policy action `Manage auto-upgrade agents` to edit `required_versions` config of agent policy on the UI. Also added it to the Agent policy details header to open the modal. To test: - enable FF in `kibana.dev.yml` - `xpack.fleet.enableExperimental: ['enableAutomaticAgentUpgrades']` - navigate to an Agent policy, click Actions and click `Manage auto-upgrade agents` <img width="1197" alt="image" src="https://github.com/user-attachments/assets/de1afa92-c155-4072-8ed6-7e8263480199" /> Added validation on the UI (same as in the API) to prevent duplicate versions, more than 100 percentages, empty percentage. Added a callout to show how many agents are impacted by the update. <img width="1044" alt="image" src="https://github.com/user-attachments/assets/c889da73-ac8f-4c74-9bc5-113cb5b902c8" /> <img width="825" alt="image" src="https://github.com/user-attachments/assets/cd9d797d-4343-44e8-bf29-b8683d5f83d4" /> Added `Auto-upgrade agents` with a `Manage` link to Agent policy details header <img width="1029" alt="image" src="https://github.com/user-attachments/assets/733a21e6-4c78-4e83-b2cf-00a757921410" /> Moved the percentage helptext to a tooltip: <img width="794" alt="image" src="https://github.com/user-attachments/assets/3e133a50-e2aa-4de3-a49d-f312648fdc21" /> - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
1 parent d126c89 commit 16a0eb3

File tree

15 files changed

+696
-171
lines changed

15 files changed

+696
-171
lines changed

x-pack/platform/plugins/shared/fleet/common/services/agent_utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* 2.0; you may not use this file except in compliance with the Elastic License
55
* 2.0.
66
*/
7+
import semverValid from 'semver/functions/valid';
8+
9+
import type { AgentTargetVersion } from '../types';
710

811
export function removeSOAttributes(kuery: string): string {
912
return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, '');
@@ -20,3 +23,25 @@ export function getSortConfig(
2023
: [];
2124
return [{ [sortField]: { order: sortOrder } }, ...secondarySort];
2225
}
26+
27+
export function checkTargetVersionsValidity(
28+
requiredVersions: AgentTargetVersion[]
29+
): string | undefined {
30+
const versions = requiredVersions.map((v) => v.version);
31+
const uniqueVersions = new Set(versions);
32+
if (versions.length !== uniqueVersions.size) {
33+
return `duplicate versions not allowed`;
34+
}
35+
if (requiredVersions.some((item) => !item.percentage)) {
36+
return `percentage is required`;
37+
}
38+
for (const version of versions) {
39+
if (!semverValid(version)) {
40+
return `invalid semver version ${version}`;
41+
}
42+
}
43+
const sumOfPercentages = requiredVersions.reduce((acc, v) => acc + v.percentage, 0);
44+
if (sumOfPercentages > 100) {
45+
return `sum of percentages cannot exceed 100`;
46+
}
47+
}

x-pack/platform/plugins/shared/fleet/common/services/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,4 @@ export {
9292
isAgentVersionLessThanFleetServer,
9393
} from './check_fleet_server_versions';
9494

95-
export { removeSOAttributes, getSortConfig } from './agent_utils';
95+
export { removeSOAttributes, getSortConfig, checkTargetVersionsValidity } from './agent_utils';

x-pack/platform/plugins/shared/fleet/cypress/e2e/agent_policy.cy.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { setupFleetServer } from '../tasks/fleet_server';
88
import { AGENT_FLYOUT, AGENT_POLICY_DETAILS_PAGE } from '../screens/fleet';
99
import { login } from '../tasks/login';
10+
import { visit } from '../tasks/common';
1011

1112
describe('Edit agent policy', () => {
1213
beforeEach(() => {
@@ -37,7 +38,7 @@ describe('Edit agent policy', () => {
3738
});
3839

3940
it('should edit agent policy', () => {
40-
cy.visit('/app/fleet/policies/policy-1/settings');
41+
visit('/app/fleet/policies/policy-1/settings');
4142
cy.get('[placeholder="Optional description"').clear().type('desc');
4243

4344
cy.intercept('/api/fleet/agent_policies/policy-1', {
@@ -135,7 +136,7 @@ describe('Edit agent policy', () => {
135136
},
136137
});
137138

138-
cy.visit('/app/fleet/policies/policy-1');
139+
visit('/app/fleet/policies/policy-1');
139140

140141
cy.getBySel(AGENT_POLICY_DETAILS_PAGE.ADD_AGENT_LINK).click();
141142
cy.getBySel(AGENT_FLYOUT.KUBERNETES_PLATFORM_TYPE).click();

x-pack/platform/plugins/shared/fleet/cypress/tasks/common.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ const disableNewFeaturesTours = (window: Window) => {
8080
});
8181
};
8282

83+
const disableFleetTours = (window: Window) => {
84+
window.localStorage.setItem('fleet.autoUpgradeAgentsTour', JSON.stringify({ active: false }));
85+
};
86+
8387
export const waitForPageToBeLoaded = () => {
8488
cy.get(LOADING_INDICATOR_HIDDEN).should('exist');
8589
cy.get(LOADING_INDICATOR).should('not.exist');
@@ -115,6 +119,7 @@ export const visit = (url: string, options: Partial<Cypress.VisitOptions> = {},
115119
options.onBeforeLoad?.(win);
116120

117121
disableNewFeaturesTours(win);
122+
disableFleetTours(win);
118123
},
119124
onLoad: (win) => {
120125
options.onLoad?.(win);

x-pack/platform/plugins/shared/fleet/cypress/tasks/navigation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* 2.0.
66
*/
77

8+
import { visit } from './common';
9+
810
export const INTEGRATIONS = 'app/integrations#/';
911
export const FLEET = 'app/fleet/';
1012
export const LOGIN_API_ENDPOINT = '/internal/security/login';
@@ -16,5 +18,5 @@ export const hostDetailsUrl = (hostName: string) =>
1618
`/app/security/hosts/${hostName}/authentications`;
1719

1820
export const navigateTo = (page: string) => {
19-
cy.visit(page);
21+
visit(page);
2022
};

x-pack/platform/plugins/shared/fleet/public/applications/fleet/layouts/default/default.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useLink, useConfig, useAuthz, useStartServices } from '../../hooks';
1515
import { WithHeaderLayout } from '../../../../layouts';
1616

1717
import { ExperimentalFeaturesService } from '../../services';
18+
import { AutoUpgradeAgentsTour } from '../../sections/agent_policy/components/auto_upgrade_agents_tour';
1819

1920
import { DefaultPageTitle } from './default_page_title';
2021

@@ -60,6 +61,7 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({
6061
isSelected: section === 'agent_policies',
6162
href: getHref('policies_list'),
6263
'data-test-subj': 'fleet-agent-policies-tab',
64+
id: 'fleet-agent-policies-tab',
6365
},
6466
{
6567
name: (
@@ -145,6 +147,7 @@ export const DefaultLayout: React.FunctionComponent<Props> = ({
145147
<WithHeaderLayout leftColumn={<DefaultPageTitle />} rightColumn={rightColumn} tabs={tabs}>
146148
{children}
147149
</WithHeaderLayout>
150+
<AutoUpgradeAgentsTour anchor="#fleet-agent-policies-tab" />
148151
</>
149152
);
150153
};

x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/agent_policy/components/actions_menu.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
1010
import { EuiContextMenuItem, EuiPortal } from '@elastic/eui';
1111

1212
import type { AgentPolicy } from '../../../types';
13-
import { useAuthz } from '../../../hooks';
13+
import { useAgentPolicyRefresh, useAuthz } from '../../../hooks';
1414
import {
1515
AgentEnrollmentFlyout,
1616
ContextMenuActions,
@@ -22,6 +22,8 @@ import { policyHasFleetServer, ExperimentalFeaturesService } from '../../../serv
2222

2323
import { AgentUpgradeAgentModal } from '../../agents/components';
2424

25+
import { ManageAutoUpgradeAgentsModal } from '../../agents/components/manage_auto_upgrade_agents_modal';
26+
2527
import { AgentPolicyYamlFlyout } from './agent_policy_yaml_flyout';
2628
import { AgentPolicyCopyProvider } from './agent_policy_copy_provider';
2729
import { AgentPolicyDeleteProvider } from './agent_policy_delete_provider';
@@ -49,6 +51,9 @@ export const AgentPolicyActionMenu = memo<{
4951
const [isUninstallCommandFlyoutOpen, setIsUninstallCommandFlyoutOpen] =
5052
useState<boolean>(false);
5153
const [isUpgradeAgentsModalOpen, setIsUpgradeAgentsModalOpen] = useState<boolean>(false);
54+
const [isManageAutoUpgradeAgentsModalOpen, setIsManageAutoUpgradeAgentsModalOpen] =
55+
useState<boolean>(false);
56+
const refreshAgentPolicy = useAgentPolicyRefresh();
5257

5358
const { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get();
5459

@@ -100,6 +105,23 @@ export const AgentPolicyActionMenu = memo<{
100105
</EuiContextMenuItem>
101106
);
102107

108+
const manageAutoUpgradeAgentsItem = (
109+
<EuiContextMenuItem
110+
icon="gear"
111+
disabled={!authz.fleet.allAgentPolicies}
112+
onClick={() => {
113+
setIsContextMenuOpen(false);
114+
setIsManageAutoUpgradeAgentsModalOpen(!isManageAutoUpgradeAgentsModalOpen);
115+
}}
116+
key="manageAutoUpgradeAgents"
117+
>
118+
<FormattedMessage
119+
id="xpack.fleet.agentPolicyActionMenu.manageAutoUpgradeAgentsText"
120+
defaultMessage="Manage auto-upgrade agents"
121+
/>
122+
</EuiContextMenuItem>
123+
);
124+
103125
const deletePolicyItem = (
104126
<AgentPolicyDeleteProvider
105127
hasFleetServer={policyHasFleetServer(agentPolicy as AgentPolicy)}
@@ -189,6 +211,7 @@ export const AgentPolicyActionMenu = memo<{
189211
)}
190212
</EuiContextMenuItem>,
191213
viewPolicyItem,
214+
manageAutoUpgradeAgentsItem,
192215
copyPolicyItem,
193216
deletePolicyItem,
194217
];
@@ -278,6 +301,20 @@ export const AgentPolicyActionMenu = memo<{
278301
/>
279302
</EuiPortal>
280303
)}
304+
{isManageAutoUpgradeAgentsModalOpen && (
305+
<EuiPortal>
306+
<ManageAutoUpgradeAgentsModal
307+
agentPolicy={agentPolicy}
308+
agentCount={agentPolicy.agents || 0}
309+
onClose={(refreshPolicy: boolean) => {
310+
setIsManageAutoUpgradeAgentsModalOpen(false);
311+
if (refreshPolicy) {
312+
refreshAgentPolicy();
313+
}
314+
}}
315+
/>
316+
</EuiPortal>
317+
)}
281318
{isUninstallCommandFlyoutOpen && (
282319
<UninstallCommandFlyout
283320
target="agent"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { EuiText, EuiTourStep } from '@elastic/eui';
9+
import React, { useState } from 'react';
10+
import { FormattedMessage } from '@kbn/i18n-react';
11+
12+
import type { TOUR_STORAGE_CONFIG } from '../../../constants';
13+
import { TOUR_STORAGE_KEYS } from '../../../constants';
14+
import { useStartServices } from '../../../hooks';
15+
16+
export const AutoUpgradeAgentsTour: React.FC<{ anchor: string }> = ({ anchor }) => {
17+
const { storage, uiSettings } = useStartServices();
18+
19+
const [tourState, setTourState] = useState({ isOpen: true });
20+
21+
const isTourHidden =
22+
uiSettings.get('hideAnnouncements', false) ||
23+
(
24+
storage.get(TOUR_STORAGE_KEYS.AUTO_UPGRADE_AGENTS) as
25+
| TOUR_STORAGE_CONFIG['AUTO_UPGRADE_AGENTS']
26+
| undefined
27+
)?.active === false;
28+
29+
const setTourAsHidden = () => {
30+
storage.set(TOUR_STORAGE_KEYS.AUTO_UPGRADE_AGENTS, {
31+
active: false,
32+
} as TOUR_STORAGE_CONFIG['AUTO_UPGRADE_AGENTS']);
33+
};
34+
35+
const onFinish = () => {
36+
setTourState({ isOpen: false });
37+
setTourAsHidden();
38+
};
39+
40+
return (
41+
<>
42+
<EuiTourStep
43+
content={
44+
<EuiText>
45+
<FormattedMessage
46+
id="xpack.fleet.autoUpgradeAgentsTour.tourContent"
47+
defaultMessage="Select your policy and configure target agent versions for automatic upgrades."
48+
/>
49+
</EuiText>
50+
}
51+
isStepOpen={!isTourHidden && tourState.isOpen}
52+
onFinish={onFinish}
53+
minWidth={360}
54+
maxWidth={360}
55+
step={1}
56+
stepsTotal={1}
57+
title={
58+
<FormattedMessage
59+
id="xpack.fleet.autoUpgradeAgentsTour.tourTitle"
60+
defaultMessage="Auto-upgrade agents"
61+
/>
62+
}
63+
anchorPosition="downLeft"
64+
anchor={anchor}
65+
/>
66+
</>
67+
);
68+
};

0 commit comments

Comments
 (0)