mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Fleet][UI] New flyout with signed uninstall command (#152886)
## Summary Adds new flyout which contains uninstall commands for Agents. 🗒️ Todo for this PR: - [x] update copies - [x] add tests Todo for follow up PRs: - update API call with real API - make general for Defend integration uninstall commands ### Screenshots #### From Agents: <img src="https://user-images.githubusercontent.com/39014407/223678946-de135d46-ee81-4b91-8102-bf3eae93a450.png" width="350px" /> <img src="https://user-images.githubusercontent.com/39014407/223678983-c94aac80-5645-4906-a4b8-998b80099de5.png" width="350px" /> #### From Agent Policies: <img src="https://user-images.githubusercontent.com/39014407/223679046-f2208cd6-bb17-42b4-bd9e-dc05adefe971.png" width="350px" /> <img src="https://user-images.githubusercontent.com/39014407/223679089-efb6d74b-8d37-449b-8d72-08f044dc1084.png" width="350px" /> #### Also, it is accessible from Policy Details, too <img src="https://user-images.githubusercontent.com/39014407/230444814-1329ba7e-6b2e-46e2-875c-8be8a84e05dc.png" width="350px" /> <img src="https://user-images.githubusercontent.com/39014407/230444849-8b1a93e9-b38d-4c01-bb44-75bbb99554c8.png" width="350px" /> ### Checklist Delete any items that are not applicable to this PR. - [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/packages/kbn-i18n/README.md) - [x] [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: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
dec3227d3d
commit
3c4276e38b
18 changed files with 686 additions and 75 deletions
|
@ -702,6 +702,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
secureLogstash: `${FLEET_DOCS}secure-logstash-connections.html`,
|
||||
agentPolicy: `${FLEET_DOCS}agent-policy.html`,
|
||||
api: `${FLEET_DOCS}fleet-api-docs.html`,
|
||||
uninstallAgent: `${SECURITY_SOLUTION_DOCS}uninstall-agent.html`,
|
||||
},
|
||||
ecs: {
|
||||
guide: `${ELASTIC_WEBSITE_URL}guide/en/ecs/current/index.html`,
|
||||
|
|
|
@ -472,6 +472,7 @@ export interface DocLinks {
|
|||
secureLogstash: string;
|
||||
agentPolicy: string;
|
||||
api: string;
|
||||
uninstallAgent: string;
|
||||
}>;
|
||||
readonly ecs: {
|
||||
readonly guide: string;
|
||||
|
|
|
@ -15,7 +15,6 @@ import { I18nProvider } from '@kbn/i18n-react';
|
|||
|
||||
import { CoreScopedHistory } from '@kbn/core/public';
|
||||
import { getStorybookContextProvider } from '@kbn/custom-integrations-plugin/storybook';
|
||||
import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
|
||||
|
||||
import { IntegrationsAppContext } from '../../public/applications/integrations/app';
|
||||
import type { FleetConfigType, FleetStartServices } from '../../public/plugin';
|
||||
|
@ -111,7 +110,7 @@ export const StorybookContext: React.FC<{ storyContext?: Parameters<DecoratorFn>
|
|||
writeIntegrationPolicies: true,
|
||||
},
|
||||
},
|
||||
guidedOnboarding: guidedOnboardingMock.createStart(),
|
||||
guidedOnboarding: {},
|
||||
}),
|
||||
[isCloudEnabled]
|
||||
);
|
||||
|
|
|
@ -11,9 +11,15 @@ import { EuiContextMenuItem, EuiPortal } from '@elastic/eui';
|
|||
|
||||
import type { AgentPolicy } from '../../../types';
|
||||
import { useAuthz } from '../../../hooks';
|
||||
import { AgentEnrollmentFlyout, ContextMenuActions } from '../../../components';
|
||||
import {
|
||||
AgentEnrollmentFlyout,
|
||||
ContextMenuActions,
|
||||
UninstallCommandFlyout,
|
||||
} from '../../../components';
|
||||
import { FLEET_SERVER_PACKAGE } from '../../../constants';
|
||||
|
||||
import { ExperimentalFeaturesService } from '../../../../../services/experimental_features';
|
||||
|
||||
import { AgentPolicyYamlFlyout } from './agent_policy_yaml_flyout';
|
||||
import { AgentPolicyCopyProvider } from './agent_policy_copy_provider';
|
||||
|
||||
|
@ -36,6 +42,10 @@ export const AgentPolicyActionMenu = memo<{
|
|||
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(
|
||||
enrollmentFlyoutOpenByDefault
|
||||
);
|
||||
const [isUninstallCommandFlyoutOpen, setIsUninstallCommandFlyoutOpen] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const { agentTamperProtectionEnabled } = ExperimentalFeaturesService.get();
|
||||
|
||||
const isFleetServerPolicy = useMemo(
|
||||
() =>
|
||||
|
@ -120,21 +130,48 @@ export const AgentPolicyActionMenu = memo<{
|
|||
/>
|
||||
</EuiContextMenuItem>,
|
||||
];
|
||||
|
||||
if (agentTamperProtectionEnabled && !agentPolicy?.is_managed) {
|
||||
menuItems.push(
|
||||
<EuiContextMenuItem
|
||||
icon="minusInCircle"
|
||||
onClick={() => {
|
||||
setIsContextMenuOpen(false);
|
||||
setIsUninstallCommandFlyoutOpen(true);
|
||||
}}
|
||||
key="getUninstallCommand"
|
||||
data-test-subj="uninstall-agents-command-menu-item"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentPolicyActionMenu.getUninstallCommand"
|
||||
defaultMessage="Uninstall agents on this policy"
|
||||
/>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isYamlFlyoutOpen ? (
|
||||
{isYamlFlyoutOpen && (
|
||||
<EuiPortal>
|
||||
<AgentPolicyYamlFlyout
|
||||
policyId={agentPolicy.id}
|
||||
onClose={() => setIsYamlFlyoutOpen(false)}
|
||||
/>
|
||||
</EuiPortal>
|
||||
) : null}
|
||||
)}
|
||||
{isEnrollmentFlyoutOpen && (
|
||||
<EuiPortal>
|
||||
<AgentEnrollmentFlyout agentPolicy={agentPolicy} onClose={onClose} />
|
||||
</EuiPortal>
|
||||
)}
|
||||
{isUninstallCommandFlyoutOpen && (
|
||||
<UninstallCommandFlyout
|
||||
target="agent"
|
||||
policyId={agentPolicy.id}
|
||||
onClose={() => setIsUninstallCommandFlyoutOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<ContextMenuActions
|
||||
isOpen={isContextMenuOpen}
|
||||
onChange={onContextMenuChange}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 type { RenderResult } from '@testing-library/react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
|
||||
import { allowedExperimentalValues } from '../../../../../../common/experimental_features';
|
||||
import { ExperimentalFeaturesService } from '../../../../../services';
|
||||
import { createFleetTestRendererMock } from '../../../../../mock';
|
||||
import type { GetAgentPoliciesResponse } from '../../../../../../common';
|
||||
|
||||
import { AgentPolicyListPage } from '.';
|
||||
|
||||
jest.mock('../../../hooks', () => ({
|
||||
...jest.requireActual('../../../hooks'),
|
||||
useGetAgentPolicies: jest.fn().mockReturnValue({
|
||||
data: {
|
||||
items: [
|
||||
{ id: 'not_managed_policy', is_managed: false, updated_at: '2023-04-06T07:19:29.892Z' },
|
||||
{ id: 'managed_policy', is_managed: true, updated_at: '2023-04-07T07:19:29.892Z' },
|
||||
],
|
||||
total: 2,
|
||||
} as GetAgentPoliciesResponse,
|
||||
isLoading: false,
|
||||
resendRequest: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('AgentPolicyListPage', () => {
|
||||
let renderResult: RenderResult;
|
||||
|
||||
const render = () => {
|
||||
const renderer = createFleetTestRendererMock();
|
||||
|
||||
// todo: this can be removed when agentTamperProtectionEnabled feature flag is enabled/deleted
|
||||
ExperimentalFeaturesService.init({
|
||||
...allowedExperimentalValues,
|
||||
agentTamperProtectionEnabled: true,
|
||||
});
|
||||
|
||||
return renderer.render(<AgentPolicyListPage />);
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
renderResult = render();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.queryByText(/Loading agent policies/)).not.toBeInTheDocument();
|
||||
expect(renderResult.queryByText(/No agent policies/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Uninstall command flyout', () => {
|
||||
it('should not render "Uninstall agents on this policy" menu item for managed Agent', async () => {
|
||||
expect(
|
||||
renderResult.queryByTestId('uninstall-agents-command-menu-item')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(renderResult.getAllByTestId('agentActionsBtn')[1]);
|
||||
|
||||
expect(
|
||||
renderResult.queryByTestId('uninstall-agents-command-menu-item')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render "Uninstall agents on this policy" menu item for not managed Agent', async () => {
|
||||
expect(
|
||||
renderResult.queryByTestId('uninstall-agents-command-menu-item')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(renderResult.getAllByTestId('agentActionsBtn')[0]);
|
||||
|
||||
expect(renderResult.queryByTestId('uninstall-agents-command-menu-item')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open uninstall commands flyout when clicking on "Uninstall agents on this policy"', () => {
|
||||
fireEvent.click(renderResult.getAllByTestId('agentActionsBtn')[0]);
|
||||
expect(renderResult.queryByTestId('uninstall-command-flyout')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(renderResult.getByTestId('uninstall-agents-command-menu-item'));
|
||||
|
||||
expect(renderResult.queryByTestId('uninstall-command-flyout')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -34,7 +34,8 @@ import {
|
|||
useUrlParams,
|
||||
useBreadcrumbs,
|
||||
} from '../../../hooks';
|
||||
import { AgentPolicySummaryLine, SearchBar } from '../../../components';
|
||||
import { SearchBar } from '../../../components';
|
||||
import { AgentPolicySummaryLine } from '../../../../../components';
|
||||
import { LinkedAgentCount, AgentPolicyActionMenu } from '../components';
|
||||
|
||||
import { CreateAgentPolicyFlyout } from './components';
|
||||
|
|
|
@ -38,7 +38,8 @@ function renderTableRowActions({
|
|||
onReassignClick={jest.fn()}
|
||||
onRequestDiagnosticsClick={jest.fn()}
|
||||
onUnenrollClick={jest.fn()}
|
||||
onUpgradeClick={jest.fn}
|
||||
onUpgradeClick={jest.fn()}
|
||||
onGetUninstallCommandClick={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ export const TableRowActions: React.FunctionComponent<{
|
|||
agentPolicy?: AgentPolicy;
|
||||
onReassignClick: () => void;
|
||||
onUnenrollClick: () => void;
|
||||
onGetUninstallCommandClick: () => void;
|
||||
onUpgradeClick: () => void;
|
||||
onAddRemoveTagsClick: (button: HTMLElement) => void;
|
||||
onRequestDiagnosticsClick: () => void;
|
||||
|
@ -30,6 +31,7 @@ export const TableRowActions: React.FunctionComponent<{
|
|||
agentPolicy,
|
||||
onReassignClick,
|
||||
onUnenrollClick,
|
||||
onGetUninstallCommandClick,
|
||||
onUpgradeClick,
|
||||
onAddRemoveTagsClick,
|
||||
onRequestDiagnosticsClick,
|
||||
|
@ -40,7 +42,8 @@ export const TableRowActions: React.FunctionComponent<{
|
|||
const isUnenrolling = agent.status === 'unenrolling';
|
||||
const kibanaVersion = useKibanaVersion();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const { diagnosticFileUploadEnabled } = ExperimentalFeaturesService.get();
|
||||
const { diagnosticFileUploadEnabled, agentTamperProtectionEnabled } =
|
||||
ExperimentalFeaturesService.get();
|
||||
const menuItems = [
|
||||
<EuiContextMenuItem
|
||||
icon="inspect"
|
||||
|
@ -113,6 +116,26 @@ export const TableRowActions: React.FunctionComponent<{
|
|||
/>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
|
||||
if (agentTamperProtectionEnabled) {
|
||||
menuItems.push(
|
||||
<EuiContextMenuItem
|
||||
icon="minusInCircle"
|
||||
onClick={() => {
|
||||
onGetUninstallCommandClick();
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
disabled={!agent.active}
|
||||
key="getUninstallCommand"
|
||||
data-test-subj="uninstallAgentMenuItem"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentList.getUninstallCommand"
|
||||
defaultMessage="Uninstall agent"
|
||||
/>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (diagnosticFileUploadEnabled) {
|
||||
|
|
|
@ -10,8 +10,10 @@ import React from 'react';
|
|||
import type { RenderResult } from '@testing-library/react';
|
||||
import { act, fireEvent, waitFor } from '@testing-library/react';
|
||||
|
||||
import { allowedExperimentalValues } from '../../../../../../common/experimental_features';
|
||||
import { ExperimentalFeaturesService } from '../../../../../services';
|
||||
import type { GetAgentPoliciesResponse } from '../../../../../../common';
|
||||
import { createFleetTestRendererMock } from '../../../../../mock';
|
||||
|
||||
import { sendGetAgents, sendGetAgentStatus } from '../../../hooks';
|
||||
|
||||
import { AgentListPage } from '.';
|
||||
|
@ -28,7 +30,12 @@ jest.mock('../../../hooks', () => ({
|
|||
},
|
||||
sendGetAgents: jest.fn(),
|
||||
useGetAgentPolicies: jest.fn().mockReturnValue({
|
||||
data: { items: [{ id: 'policy1' }] },
|
||||
data: {
|
||||
items: [
|
||||
{ id: 'policy1', is_managed: false },
|
||||
{ id: 'managed_policy', is_managed: true },
|
||||
],
|
||||
} as GetAgentPoliciesResponse,
|
||||
isLoading: false,
|
||||
resendRequest: jest.fn(),
|
||||
}),
|
||||
|
@ -94,18 +101,63 @@ describe('agent_list_page', () => {
|
|||
|
||||
let utils: RenderResult;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockedSendGetAgents
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
items: mapAgents(['agent1', 'agent2', 'agent3', 'agent4', 'agent5']),
|
||||
total: 6,
|
||||
statusSummary: {
|
||||
online: 6,
|
||||
describe('handling slow agent status request', () => {
|
||||
beforeEach(async () => {
|
||||
mockedSendGetAgents
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
items: mapAgents(['agent1', 'agent2', 'agent3', 'agent4', 'agent5']),
|
||||
total: 6,
|
||||
statusSummary: {
|
||||
online: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
items: mapAgents(['agent1', 'agent2', 'agent3', 'agent4', 'agent6']),
|
||||
total: 6,
|
||||
statusSummary: {
|
||||
online: 6,
|
||||
},
|
||||
},
|
||||
});
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not send another agents status request if first one takes longer', () => {
|
||||
mockedSendGetAgentStatus.mockImplementation(async () => {
|
||||
const sleep = () => {
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => res({}), 35000);
|
||||
});
|
||||
};
|
||||
await sleep();
|
||||
return {
|
||||
data: {
|
||||
results: {
|
||||
inactive: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
({ utils } = renderAgentList());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(65000);
|
||||
});
|
||||
|
||||
expect(mockedSendGetAgentStatus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selection change', () => {
|
||||
beforeEach(async () => {
|
||||
mockedSendGetAgents.mockResolvedValue({
|
||||
data: {
|
||||
items: mapAgents(['agent1', 'agent2', 'agent3', 'agent4', 'agent6']),
|
||||
total: 6,
|
||||
|
@ -114,40 +166,6 @@ describe('agent_list_page', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should not send another agents status request if first one takes longer', () => {
|
||||
mockedSendGetAgentStatus.mockImplementation(async () => {
|
||||
const sleep = () => {
|
||||
return new Promise((res) => {
|
||||
setTimeout(() => res({}), 35000);
|
||||
});
|
||||
};
|
||||
await sleep();
|
||||
return {
|
||||
data: {
|
||||
results: {
|
||||
inactive: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
({ utils } = renderAgentList());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(65000);
|
||||
});
|
||||
|
||||
expect(mockedSendGetAgentStatus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('selection change', () => {
|
||||
beforeEach(async () => {
|
||||
mockedSendGetAgentStatus.mockResolvedValue({
|
||||
data: {
|
||||
results: {
|
||||
|
@ -162,18 +180,14 @@ describe('agent_list_page', () => {
|
|||
expect(utils.getByText('Showing 6 agents')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
const selectAll = utils.container.querySelector('[data-test-subj="checkboxSelectAll"]');
|
||||
fireEvent.click(selectAll!);
|
||||
});
|
||||
const selectAll = utils.container.querySelector('[data-test-subj="checkboxSelectAll"]');
|
||||
fireEvent.click(selectAll!);
|
||||
|
||||
await waitFor(() => {
|
||||
utils.getByText('5 agents selected');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(utils.getByText('Select everything on all pages'));
|
||||
});
|
||||
fireEvent.click(utils.getByText('Select everything on all pages'));
|
||||
utils.getByText('All agents selected');
|
||||
});
|
||||
|
||||
|
@ -198,17 +212,13 @@ describe('agent_list_page', () => {
|
|||
});
|
||||
|
||||
it('should set selection mode when agent selection changed manually', async () => {
|
||||
act(() => {
|
||||
fireEvent.click(utils.getAllByRole('checkbox')[3]);
|
||||
});
|
||||
fireEvent.click(utils.getAllByRole('checkbox')[3]);
|
||||
|
||||
utils.getByText('4 agents selected');
|
||||
});
|
||||
|
||||
it('should pass sort parameters on table sort', () => {
|
||||
act(() => {
|
||||
fireEvent.click(utils.getByTitle('Last activity'));
|
||||
});
|
||||
fireEvent.click(utils.getByTitle('Last activity'));
|
||||
|
||||
expect(mockedSendGetAgents).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -219,9 +229,7 @@ describe('agent_list_page', () => {
|
|||
});
|
||||
|
||||
it('should pass keyword field on table sort on version', () => {
|
||||
act(() => {
|
||||
fireEvent.click(utils.getByTitle('Version'));
|
||||
});
|
||||
fireEvent.click(utils.getByTitle('Version'));
|
||||
|
||||
expect(mockedSendGetAgents).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -232,9 +240,7 @@ describe('agent_list_page', () => {
|
|||
});
|
||||
|
||||
it('should pass keyword field on table sort on hostname', () => {
|
||||
act(() => {
|
||||
fireEvent.click(utils.getByTitle('Host'));
|
||||
});
|
||||
fireEvent.click(utils.getByTitle('Host'));
|
||||
|
||||
expect(mockedSendGetAgents).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
@ -244,4 +250,75 @@ describe('agent_list_page', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Uninstall agent', () => {
|
||||
let renderResult: RenderResult;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockedSendGetAgents.mockResolvedValue({
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: 'agent1',
|
||||
active: true,
|
||||
policy_id: 'policy1',
|
||||
local_metadata: { host: { hostname: 'agent1' } },
|
||||
},
|
||||
{
|
||||
id: 'agent2',
|
||||
active: true,
|
||||
policy_id: 'managed_policy',
|
||||
local_metadata: { host: { hostname: 'agent2' } },
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
statusSummary: {
|
||||
online: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
mockedSendGetAgentStatus.mockResolvedValue({
|
||||
data: { results: { inactive: 0 }, totalInactive: 0 },
|
||||
});
|
||||
|
||||
const renderer = createFleetTestRendererMock();
|
||||
|
||||
// todo: this can be removed when agentTamperProtectionEnabled feature flag is enabled/deleted
|
||||
ExperimentalFeaturesService.init({
|
||||
...allowedExperimentalValues,
|
||||
agentTamperProtectionEnabled: true,
|
||||
});
|
||||
|
||||
renderResult = renderer.render(<AgentListPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.queryByText('Showing 2 agents')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render "Uninstall agent" menu item for managed Agent', async () => {
|
||||
expect(renderResult.queryByTestId('uninstallAgentMenuItem')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(renderResult.getAllByTestId('agentActionsBtn')[1]);
|
||||
|
||||
expect(renderResult.queryByTestId('uninstallAgentMenuItem')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render "Uninstall agent" menu item for not managed Agent', async () => {
|
||||
expect(renderResult.queryByTestId('uninstallAgentMenuItem')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(renderResult.getAllByTestId('agentActionsBtn')[0]);
|
||||
|
||||
expect(renderResult.queryByTestId('uninstallAgentMenuItem')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should open uninstall commands flyout when clicking on "Uninstall agent"', () => {
|
||||
fireEvent.click(renderResult.getAllByTestId('agentActionsBtn')[0]);
|
||||
expect(renderResult.queryByTestId('uninstall-command-flyout')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(renderResult.getByTestId('uninstallAgentMenuItem'));
|
||||
|
||||
expect(renderResult.queryByTestId('uninstall-command-flyout')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
sendGetAgentTags,
|
||||
useFleetServerStandalone,
|
||||
} from '../../../hooks';
|
||||
import { AgentEnrollmentFlyout } from '../../../components';
|
||||
import { AgentEnrollmentFlyout, UninstallCommandFlyout } from '../../../components';
|
||||
import {
|
||||
AgentStatusKueryHelper,
|
||||
ExperimentalFeaturesService,
|
||||
|
@ -133,6 +133,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
// Agent actions states
|
||||
const [agentToReassign, setAgentToReassign] = useState<Agent | undefined>(undefined);
|
||||
const [agentToUnenroll, setAgentToUnenroll] = useState<Agent | undefined>(undefined);
|
||||
const [agentToGetUninstallCommand, setAgentToGetUninstallCommand] = useState<Agent | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [agentToUpgrade, setAgentToUpgrade] = useState<Agent | undefined>(undefined);
|
||||
const [agentToAddRemoveTags, setAgentToAddRemoveTags] = useState<Agent | undefined>(undefined);
|
||||
const [tagsPopoverButton, setTagsPopoverButton] = useState<HTMLElement>();
|
||||
|
@ -207,6 +210,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
setAgentToAddRemoveTags(agent);
|
||||
setShowTagsAddRemove(!showTagsAddRemove);
|
||||
}}
|
||||
onGetUninstallCommandClick={() => setAgentToGetUninstallCommand(agent)}
|
||||
onRequestDiagnosticsClick={() => setAgentToRequestDiagnostics(agent)}
|
||||
/>
|
||||
);
|
||||
|
@ -453,6 +457,18 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
/>
|
||||
</EuiPortal>
|
||||
)}
|
||||
{agentToGetUninstallCommand && (
|
||||
<EuiPortal>
|
||||
<UninstallCommandFlyout
|
||||
target="agent"
|
||||
policyId={agentToGetUninstallCommand.policy_id}
|
||||
onClose={() => {
|
||||
setAgentToGetUninstallCommand(undefined);
|
||||
refreshAgents({ refreshTags: true });
|
||||
}}
|
||||
/>
|
||||
</EuiPortal>
|
||||
)}
|
||||
{agentToUpgrade && (
|
||||
<EuiPortal>
|
||||
<AgentUpgradeAgentModal
|
||||
|
|
|
@ -28,3 +28,4 @@ export { ConfirmForceInstallModal } from './confirm_force_install_modal';
|
|||
export { DevtoolsRequestFlyoutButton } from './devtools_request_flyout';
|
||||
export { HeaderReleaseBadge, InlineReleaseBadge } from './release_badge';
|
||||
export { WithGuidedOnboardingTour } from './with_guided_onboarding_tour';
|
||||
export { UninstallCommandFlyout } from './uninstall_command_flyout';
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { EuiButtonGroup, EuiCodeBlock, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import type { PLATFORM_TYPE } from '../../hooks';
|
||||
import { PLATFORM_OPTIONS, usePlatform } from '../../hooks';
|
||||
|
||||
import type { Commands } from './types';
|
||||
|
||||
interface Props {
|
||||
commands: Commands;
|
||||
}
|
||||
|
||||
export const CommandsForPlatforms: React.FunctionComponent<Props> = ({ commands }) => {
|
||||
const { platform, setPlatform } = usePlatform();
|
||||
|
||||
const options = useMemo(
|
||||
() => PLATFORM_OPTIONS.filter(({ id }) => commands[id as PLATFORM_TYPE]),
|
||||
[commands]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonGroup
|
||||
options={options}
|
||||
idSelected={platform}
|
||||
onChange={(id) => setPlatform(id as PLATFORM_TYPE)}
|
||||
legend={i18n.translate('xpack.fleet.agentUninstallCommandFlyout.platformSelectAriaLabel', {
|
||||
defaultMessage: 'Platform',
|
||||
})}
|
||||
data-test-subj="uninstall-commands-flyout-platforms-btn-group"
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiCodeBlock fontSize="m" isCopyable paddingSize="m">
|
||||
{commands[platform]}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
|
||||
import { REDUCED_PLATFORM_OPTIONS } from '../../hooks';
|
||||
|
||||
import type { Commands, UninstallCommandTarget } from './types';
|
||||
|
||||
// todo: update with real API
|
||||
export const useCommands = (
|
||||
policyId: string | undefined,
|
||||
target: UninstallCommandTarget
|
||||
): Commands => {
|
||||
const commands = useMemo(
|
||||
() =>
|
||||
REDUCED_PLATFORM_OPTIONS.map(({ id }) => id).reduce<Commands>(
|
||||
(_commands, platform) => ({
|
||||
..._commands,
|
||||
[platform]: policyId
|
||||
? `${platform}/${target} command for ${policyId}`
|
||||
: `${platform}/${target} command`,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
[policyId, target]
|
||||
);
|
||||
|
||||
return commands;
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { UninstallCommandFlyout } from './uninstall_command_flyout';
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 type { PLATFORM_TYPE } from '../../hooks';
|
||||
|
||||
export const UNINSTALL_COMMAND_TARGETS = ['agent', 'endpoint'] as const;
|
||||
export type UninstallCommandTarget = typeof UNINSTALL_COMMAND_TARGETS[number];
|
||||
|
||||
export type Commands = {
|
||||
[key in PLATFORM_TYPE]?: string;
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 type { UninstallCommandFlyoutProps } from './uninstall_command_flyout';
|
||||
import { UNINSTALL_COMMAND_TARGETS } from './types';
|
||||
import { UninstallCommandFlyout } from './uninstall_command_flyout';
|
||||
|
||||
export default {
|
||||
component: UninstallCommandFlyout,
|
||||
title: 'Sections/Fleet/Uninstall command flyout',
|
||||
argTypes: {
|
||||
target: {
|
||||
options: UNINSTALL_COMMAND_TARGETS,
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
onClose: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
interface Story {
|
||||
args: Partial<UninstallCommandFlyoutProps>;
|
||||
}
|
||||
|
||||
export const ForAgent: Story = {
|
||||
args: {
|
||||
target: 'agent',
|
||||
policyId: 'policy-id-1',
|
||||
},
|
||||
};
|
||||
|
||||
export const ForEndpoint: Story = {
|
||||
args: {
|
||||
target: 'endpoint',
|
||||
policyId: 'policy-id-2',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 { createFleetTestRendererMock } from '../../mock';
|
||||
|
||||
import type { UninstallCommandFlyoutProps } from './uninstall_command_flyout';
|
||||
import { UninstallCommandFlyout } from './uninstall_command_flyout';
|
||||
import { useCommands } from './hooks';
|
||||
import type { Commands } from './types';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useCommands: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('UninstallCommandFlyout', () => {
|
||||
const defaultCommands: Commands = {
|
||||
linux: 'command for linux',
|
||||
mac: 'command for mac',
|
||||
deb: 'commands for deb',
|
||||
windows: 'commands for windows',
|
||||
rpm: 'commands for rpm',
|
||||
};
|
||||
const useCommandsMock = useCommands as jest.Mock;
|
||||
|
||||
const render = (props: Partial<UninstallCommandFlyoutProps> = {}) => {
|
||||
const renderer = createFleetTestRendererMock();
|
||||
|
||||
return renderer.render(
|
||||
<UninstallCommandFlyout onClose={() => {}} target="agent" policyId="-" {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useCommandsMock.mockReturnValue(defaultCommands);
|
||||
});
|
||||
|
||||
describe('uninstall command targets', () => {
|
||||
it('renders flyout for Agent', () => {
|
||||
const renderResult = render({ target: 'agent' });
|
||||
|
||||
expect(renderResult.queryByText(/Uninstall Elastic Agent on your host/)).toBeInTheDocument();
|
||||
expect(renderResult.queryByText(/Uninstall Elastic Defend/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders flyout for Endpoint integration', () => {
|
||||
const renderResult = render({ target: 'endpoint' });
|
||||
|
||||
expect(renderResult.queryByText(/Uninstall Elastic Defend/)).toBeInTheDocument();
|
||||
expect(
|
||||
renderResult.queryByText(/Uninstall Elastic Agent on your host/)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering commands only for received platforms', () => {
|
||||
it('renders commands for e.g. Linux and Mac', () => {
|
||||
useCommandsMock.mockReturnValue({
|
||||
linux: 'command for linux',
|
||||
mac: 'command for mac',
|
||||
});
|
||||
|
||||
const renderResult = render();
|
||||
|
||||
const platformsButtonGroup = renderResult.getByTestId(
|
||||
'uninstall-commands-flyout-platforms-btn-group'
|
||||
);
|
||||
expect(platformsButtonGroup).toHaveTextContent('Mac');
|
||||
expect(platformsButtonGroup).toHaveTextContent('Linux');
|
||||
expect(platformsButtonGroup).not.toHaveTextContent('Windows');
|
||||
expect(platformsButtonGroup).not.toHaveTextContent('RPM');
|
||||
expect(platformsButtonGroup).not.toHaveTextContent('DEB');
|
||||
});
|
||||
|
||||
it('renders commands for e.g. Mac, Windows, DEB and RPM', () => {
|
||||
useCommandsMock.mockReturnValue({
|
||||
mac: 'command for mac',
|
||||
deb: 'commands for deb',
|
||||
windows: 'commands for windows',
|
||||
rpm: 'commands for rpm',
|
||||
});
|
||||
|
||||
const renderResult = render();
|
||||
|
||||
const platformsButtonGroup = renderResult.getByTestId(
|
||||
'uninstall-commands-flyout-platforms-btn-group'
|
||||
);
|
||||
expect(platformsButtonGroup).toHaveTextContent('Mac');
|
||||
expect(platformsButtonGroup).toHaveTextContent('Windows');
|
||||
expect(platformsButtonGroup).toHaveTextContent('RPM');
|
||||
expect(platformsButtonGroup).toHaveTextContent('DEB');
|
||||
expect(platformsButtonGroup).not.toHaveTextContent('Linux');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { useStartServices } from '../../hooks';
|
||||
|
||||
import { CommandsForPlatforms } from './commands_for_platforms';
|
||||
import { useCommands } from './hooks';
|
||||
import type { UninstallCommandTarget } from './types';
|
||||
|
||||
const UninstallAgentDescription = () => {
|
||||
const { docLinks } = useStartServices();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUninstallCommandFlyout.firstParagraph"
|
||||
defaultMessage="Uninstall Elastic Agent and unenroll in Fleet to stop communicating with the host."
|
||||
/>
|
||||
</p>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUninstallCommandFlyout.subtitle"
|
||||
defaultMessage="Uninstall Elastic Agent on your host"
|
||||
/>
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUninstallCommandFlyout.description"
|
||||
defaultMessage="Select the appropriate platform and run the command to uninstall Elastic Agent. Reuse the command to uninstall agents on more than one host. {learnMoreLink}"
|
||||
values={{
|
||||
learnMoreLink: (
|
||||
<EuiLink href={docLinks.links.fleet.uninstallAgent} target="_blank">
|
||||
{i18n.translate('xpack.fleet.agentUninstallCommandFlyout.learnMore', {
|
||||
defaultMessage: 'Learn more',
|
||||
})}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UninstallEndpointDescription = () => (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.endpointUninstallCommandFlyout.subtitle"
|
||||
defaultMessage="Uninstall Elastic Defend integration on your host"
|
||||
/>
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.endpointUninstallCommandFlyout.description"
|
||||
defaultMessage="Use the below uninstall command to uninstall Endpoint integration... [TODO]"
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
export interface UninstallCommandFlyoutProps {
|
||||
target: UninstallCommandTarget;
|
||||
policyId?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const UninstallCommandFlyout: React.FunctionComponent<UninstallCommandFlyoutProps> = ({
|
||||
policyId,
|
||||
onClose,
|
||||
target,
|
||||
}) => {
|
||||
const commands = useCommands(policyId, target);
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} data-test-subj="uninstall-command-flyout">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentUninstallCommandFlyout.title"
|
||||
defaultMessage="Uninstall agent"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody>
|
||||
<EuiText>
|
||||
{target === 'agent' ? <UninstallAgentDescription /> : <UninstallEndpointDescription />}
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<CommandsForPlatforms commands={commands} />
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue