mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Defend Workflows][Reusable integrations] Handling resuable integrations on endpoint onboarding page (#193518)
## Summary During onboarding on the Endpoint list page, if the user already created at least one Elastic Defend integration, we show this screen (original): <img width="400" alt="image" src="https://github.com/user-attachments/assets/98f04002-7c18-48bf-8b29-a4ad6113385c"> Due to the new enterprise level feature added by Fleet team, the reusable integrations, now an integration policy can be assigned **zero, one or more agent policies**, and this PR's goal is to update this onboarding screen to tackle these changes. ### One integration is added to more than one Agent Policies When creating/editing an integration, it can be added to more than one Agent Policies: <img width="600" alt="image" src="https://github.com/user-attachments/assets/f0982e28-11b0-4aef-a059-6c952e0e33b7"> #### ✅ Solution for usecase To be able to select where to enroll an Agent, now they are listed as `Package policy - Agent policy` pairs. <img width="600" alt="image" src="https://github.com/user-attachments/assets/c82d70ed-5b87-43b5-ab16-3c4549373b32"> ### Some integrations are not added to an Agent Policy <img width="400" alt="image" src="https://github.com/user-attachments/assets/108ccd78-d019-42ca-a92a-905344172d09"> #### ✅ Solution for usecase A new callout is added to indicate to the user that there are some integrations that cannot be deployed to an Agent. <img width="600" alt="image" src="https://github.com/user-attachments/assets/3bedc56b-70a3-4f4f-9881-e91ae458cadf"> Clicking on the Integrations opens their editing page in a new tab. Clickin on the 'Elastic Defend Integration policies' opens the Defend integration's policies tab in a new browser tab. ### None of the integrations are added to an Agent Policy <img width="300" alt="image" src="https://github.com/user-attachments/assets/39fd2efc-2a96-4109-9311-b666bd44ca1f"> #### ✅ Solution for usecase Another 'warning' callout is displayed indicating that there are no usable integrations. This, combined with the other callout hopefully help the user to go forward. <img width="600" alt="image" src="https://github.com/user-attachments/assets/78efc1ea-dd3e-4aff-bd67-b1e37a2508e1"> ### RBAC In case the user doesn't hold the required privileges, the same screen is displayed as when there are no hosts and no policies, or there are policies but no hosts. Just as before. <img width="600" alt="image" src="https://github.com/user-attachments/assets/814f460d-7e17-49a7-8337-9cf1f8a8f0ef"> ### 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] [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 - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Joe Peeples <joe.peeples@elastic.co>
This commit is contained in:
parent
d570d60b0b
commit
8eceb0db4d
11 changed files with 675 additions and 303 deletions
|
@ -14,7 +14,7 @@ type PartialPackagePolicy = Partial<Omit<PackagePolicy, 'inputs'>> & {
|
|||
inputs?: PackagePolicy['inputs'];
|
||||
};
|
||||
|
||||
type PartialEndpointPolicyData = Partial<Omit<PolicyData, 'inputs'>> & {
|
||||
export type PartialEndpointPolicyData = Partial<Omit<PolicyData, 'inputs'>> & {
|
||||
inputs?: PolicyData['inputs'];
|
||||
};
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
import { firstNonNullValue } from './models/ecs_safety_helpers';
|
||||
import type { EventOptions } from './types/generator';
|
||||
import { BaseDataGenerator } from './data_generators/base_data_generator';
|
||||
import type { PartialEndpointPolicyData } from './data_generators/fleet_package_policy_generator';
|
||||
import { FleetPackagePolicyGenerator } from './data_generators/fleet_package_policy_generator';
|
||||
|
||||
export type Event = AlertEvent | SafeEndpointEvent;
|
||||
|
@ -1581,8 +1582,14 @@ export class EndpointDocGenerator extends BaseDataGenerator {
|
|||
/**
|
||||
* Generates a Fleet `package policy` that includes the Endpoint Policy data
|
||||
*/
|
||||
public generatePolicyPackagePolicy(seed: string = 'seed'): PolicyData {
|
||||
return new FleetPackagePolicyGenerator(seed).generateEndpointPackagePolicy();
|
||||
public generatePolicyPackagePolicy({
|
||||
seed,
|
||||
overrides,
|
||||
}: {
|
||||
seed?: string;
|
||||
overrides?: PartialEndpointPolicyData;
|
||||
} = {}): PolicyData {
|
||||
return new FleetPackagePolicyGenerator(seed).generateEndpointPackagePolicy(overrides);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,12 +22,16 @@ import {
|
|||
EuiLoadingSpinner,
|
||||
EuiLink,
|
||||
EuiSkeletonText,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { INTEGRATIONS_PLUGIN_ID } from '@kbn/fleet-plugin/common';
|
||||
import { pagePathGetters } from '@kbn/fleet-plugin/public';
|
||||
import type { ImmutableArray, PolicyData } from '../../../common/endpoint/types';
|
||||
import { useUserPrivileges } from '../../common/components/user_privileges';
|
||||
import onboardingLogo from '../images/security_administration_onboarding.svg';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { useAppUrl, useKibana } from '../../common/lib/kibana';
|
||||
|
||||
const TEXT_ALIGN_CENTER: CSSProperties = Object.freeze({
|
||||
textAlign: 'center',
|
||||
|
@ -103,12 +107,12 @@ const PolicyEmptyState = React.memo<{
|
|||
{policyEntryPoint ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromPolicyPage"
|
||||
defaultMessage="From this page, you’ll be able to view and manage the Elastic Defend Integration policies in your environment running Elastic Defend."
|
||||
defaultMessage="From this page, you can view and manage the Elastic Defend integration policies in your environment running Elastic Defend."
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromEndpointPage"
|
||||
defaultMessage="From this page, you’ll be able to view and manage the hosts in your environment running Elastic Defend."
|
||||
defaultMessage="From this page, you can view and manage the hosts in your environment running Elastic Defend."
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
|
@ -170,107 +174,216 @@ const EndpointsEmptyState = React.memo<{
|
|||
actionDisabled: boolean;
|
||||
handleSelectableOnChange: (o: EuiSelectableProps['options']) => void;
|
||||
selectionOptions: EuiSelectableProps['options'];
|
||||
}>(({ loading, onActionClick, actionDisabled, handleSelectableOnChange, selectionOptions }) => {
|
||||
const policySteps = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.list.stepOneTitle', {
|
||||
defaultMessage: 'Select the integration you want to use',
|
||||
}),
|
||||
children: (
|
||||
policyItems: ImmutableArray<PolicyData>;
|
||||
}>(
|
||||
({
|
||||
loading,
|
||||
onActionClick,
|
||||
actionDisabled,
|
||||
handleSelectableOnChange,
|
||||
selectionOptions,
|
||||
policyItems,
|
||||
}) => {
|
||||
const { getAppUrl } = useAppUrl();
|
||||
const policyItemsWithoutAgentPolicy = useMemo(
|
||||
() => policyItems.filter((policy) => !policy.policy_ids.length),
|
||||
[policyItems]
|
||||
);
|
||||
|
||||
const policiesNotAddedToAgentPolicyCallout = useMemo(
|
||||
() =>
|
||||
!!policyItemsWithoutAgentPolicy.length && (
|
||||
<>
|
||||
<EuiText color="subdued" size="m" grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.stepOne"
|
||||
defaultMessage="Select from existing integrations. This can be changed later."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSelectable
|
||||
options={selectionOptions}
|
||||
singleSelection="always"
|
||||
isLoading={loading}
|
||||
height={100}
|
||||
listProps={{ bordered: true, singleSelection: true }}
|
||||
onChange={handleSelectableOnChange}
|
||||
data-test-subj="onboardingPolicySelect"
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiCallOut
|
||||
color="primary"
|
||||
iconType="iInCircle"
|
||||
title={i18n.translate(
|
||||
'xpack.securitySolution.endpoint.list.notAddedIntegrations.title',
|
||||
{
|
||||
defaultMessage: 'Integrations not added to an Agent policy',
|
||||
}
|
||||
)}
|
||||
data-test-subj="integrationsNotAddedToAgentPolicyCallout"
|
||||
>
|
||||
{(list) => {
|
||||
return loading ? (
|
||||
<EuiSelectableMessage>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.loadingPolicies"
|
||||
defaultMessage="Loading integrations"
|
||||
/>
|
||||
</EuiSelectableMessage>
|
||||
) : selectionOptions.length ? (
|
||||
list
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.noPolicies"
|
||||
defaultMessage="There are no integrations."
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</EuiSelectable>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.list.stepTwoTitle', {
|
||||
defaultMessage: 'Enroll your agents enabled with Elastic Defend through Fleet',
|
||||
}),
|
||||
status: actionDisabled ? 'disabled' : '',
|
||||
children: (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="m" grow={false}>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiText size="s" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.stepTwo"
|
||||
defaultMessage="You’ll be provided with the necessary commands to get started."
|
||||
id="xpack.securitySolution.endpoint.list.notAddedIntegrations.description"
|
||||
defaultMessage="The following Elastic Defend integrations aren't added to an Agent policy, so they can't be deployed to an Agent. Click on an integration to edit it, and add it to an Agent policy:"
|
||||
/>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<ul>
|
||||
{policyItemsWithoutAgentPolicy.map((policyItem) => (
|
||||
<li key={policyItem.id}>
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href={getAppUrl({
|
||||
appId: INTEGRATIONS_PLUGIN_ID,
|
||||
path: pagePathGetters.integration_policy_edit({
|
||||
packagePolicyId: policyItem.id,
|
||||
})[1],
|
||||
})}
|
||||
data-test-subj="integrationWithoutAgentPolicyListItem"
|
||||
>
|
||||
{policyItem.name}
|
||||
</EuiLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.notAddedIntegrations.visitIntegrations"
|
||||
defaultMessage="You can also view a list of all {integrationPolicies}."
|
||||
values={{
|
||||
integrationPolicies: (
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href={getAppUrl({
|
||||
appId: INTEGRATIONS_PLUGIN_ID,
|
||||
path: pagePathGetters.integration_details_policies({
|
||||
pkgkey: 'endpoint',
|
||||
})[1],
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.notAddedIntegrations.integrationPolicies"
|
||||
defaultMessage="Elastic Defend integration policies"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={onActionClick}
|
||||
isDisabled={actionDisabled}
|
||||
data-test-subj="onboardingStartButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.emptyCreateNewButton"
|
||||
defaultMessage="Enroll Agent"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCallOut>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
[selectionOptions, handleSelectableOnChange, loading, actionDisabled, onActionClick]
|
||||
);
|
||||
[getAppUrl, policyItemsWithoutAgentPolicy]
|
||||
);
|
||||
|
||||
return (
|
||||
<ManagementEmptyState
|
||||
loading={loading}
|
||||
dataTestSubj="emptyHostsTable"
|
||||
steps={policySteps}
|
||||
headerComponent={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.noEndpointsPrompt"
|
||||
defaultMessage="Next step: Enroll an Agent with Elastic Defend"
|
||||
/>
|
||||
}
|
||||
bodyComponent={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.noEndpointsInstructions"
|
||||
defaultMessage="You’ve added the Elastic Defend integration. Now enroll your agents using the steps below."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const policySteps = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.list.stepOneTitle', {
|
||||
defaultMessage: 'Select the integration you want to use',
|
||||
}),
|
||||
children: (
|
||||
<>
|
||||
<EuiText color="subdued" size="m" grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.stepOne"
|
||||
defaultMessage="Select from existing integrations. This can be changed later."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiSelectable
|
||||
options={selectionOptions}
|
||||
singleSelection="always"
|
||||
isLoading={loading}
|
||||
listProps={{ bordered: true, singleSelection: true }}
|
||||
onChange={handleSelectableOnChange}
|
||||
data-test-subj="onboardingPolicySelect"
|
||||
>
|
||||
{(list) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<EuiSelectableMessage>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.loadingPolicies"
|
||||
defaultMessage="Loading integrations"
|
||||
/>
|
||||
</EuiSelectableMessage>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectionOptions.length) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
data-test-subj="noIntegrationsAddedToAgentPoliciesCallout"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.noPoliciesAssignedToAgentPolicies"
|
||||
defaultMessage="There are no Elastic Defend integrations added to Agent policies. To deploy Elastic Defend, add it to an Agent policy."
|
||||
/>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
return list;
|
||||
}}
|
||||
</EuiSelectable>
|
||||
|
||||
{policiesNotAddedToAgentPolicyCallout}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.list.stepTwoTitle', {
|
||||
defaultMessage: 'Enroll your agents enabled with Elastic Defend through Fleet',
|
||||
}),
|
||||
status: actionDisabled ? 'disabled' : '',
|
||||
children: (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="m" grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.stepTwo"
|
||||
defaultMessage="You'll be provided with the necessary commands to get started."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={onActionClick}
|
||||
isDisabled={actionDisabled}
|
||||
data-test-subj="onboardingStartButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.emptyCreateNewButton"
|
||||
defaultMessage="Enroll Agent"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
selectionOptions,
|
||||
loading,
|
||||
handleSelectableOnChange,
|
||||
policiesNotAddedToAgentPolicyCallout,
|
||||
actionDisabled,
|
||||
onActionClick,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<ManagementEmptyState
|
||||
loading={loading}
|
||||
dataTestSubj="emptyHostsTable"
|
||||
steps={policySteps}
|
||||
headerComponent={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.noEndpointsPrompt"
|
||||
defaultMessage="Next step: Enroll an Agent with Elastic Defend"
|
||||
/>
|
||||
}
|
||||
bodyComponent={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.noEndpointsInstructions"
|
||||
defaultMessage="You've added the Elastic Defend integration. Now enroll your agents using the steps below."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const ManagementEmptyState = React.memo<{
|
||||
loading: boolean;
|
||||
|
@ -284,7 +397,11 @@ const ManagementEmptyState = React.memo<{
|
|||
{loading ? (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="xl" className="essentialAnimation" />
|
||||
<EuiLoadingSpinner
|
||||
size="xl"
|
||||
className="essentialAnimation"
|
||||
data-test-subj="management-empty-state-loading-spinner"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
|
|
|
@ -156,10 +156,6 @@ const getAgentAndPoliciesForEndpointsList = async (
|
|||
return;
|
||||
}
|
||||
|
||||
// We use the Agent Policy API here, instead of the Package Policy, because we can't use
|
||||
// filter by ID of the Saved Object. Agent Policy, however, keeps a reference (array) of
|
||||
// Package Ids that it uses, thus if a reference exists there, then the package policy (policy)
|
||||
// exists.
|
||||
const policiesFound = (
|
||||
await sendBulkGetPackagePolicies(http, policyIdsToCheck)
|
||||
).items.reduce<PolicyIds>(
|
||||
|
|
|
@ -10,6 +10,7 @@ import * as reactTestingLibrary from '@testing-library/react';
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { EndpointList } from '.';
|
||||
import { createUseUiSetting$Mock } from '../../../../common/lib/kibana/kibana_react.mock';
|
||||
import type { DeepPartial } from '@kbn/utility-types';
|
||||
|
||||
import {
|
||||
mockEndpointDetailsApiResult,
|
||||
|
@ -57,6 +58,8 @@ import { getEndpointPrivilegesInitialStateMock } from '../../../../common/compon
|
|||
import { useGetEndpointDetails } from '../../../hooks/endpoint/use_get_endpoint_details';
|
||||
import { useGetAgentStatus as _useGetAgentStatus } from '../../../hooks/agents/use_get_agent_status';
|
||||
import { agentStatusMocks } from '../../../../../common/endpoint/service/response_actions/mocks/agent_status.mocks';
|
||||
import { useBulkGetAgentPolicies } from '../../../services/policies/hooks';
|
||||
import type { PartialEndpointPolicyData } from '../../../../../common/endpoint/data_generators/fleet_package_policy_generator';
|
||||
|
||||
const mockUserPrivileges = useUserPrivileges as jest.Mock;
|
||||
// not sure why this can't be imported from '../../../../common/mock/formatted_relative';
|
||||
|
@ -85,6 +88,14 @@ jest.mock('../../../services/policies/ingest', () => {
|
|||
jest.mock('../../../hooks/agents/use_get_agent_status');
|
||||
const useGetAgentStatusMock = _useGetAgentStatus as jest.Mock;
|
||||
|
||||
jest.mock('../../../services/policies/hooks', () => ({
|
||||
...jest.requireActual('../../../services/policies/hooks'),
|
||||
useBulkGetAgentPolicies: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
const useBulkGetAgentPoliciesMock = useBulkGetAgentPolicies as unknown as jest.Mock<
|
||||
DeepPartial<ReturnType<typeof useBulkGetAgentPolicies>>
|
||||
>;
|
||||
|
||||
const mockUseUiSetting$ = useUiSetting$ as jest.Mock;
|
||||
const timepickerRanges = [
|
||||
{
|
||||
|
@ -149,6 +160,7 @@ describe('when on the endpoint list page', () => {
|
|||
const { act, screen, fireEvent } = reactTestingLibrary;
|
||||
|
||||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: reactTestingLibrary.RenderResult;
|
||||
let history: AppContextTestRender['history'];
|
||||
let store: AppContextTestRender['store'];
|
||||
let coreStart: AppContextTestRender['coreStart'];
|
||||
|
@ -170,7 +182,7 @@ describe('when on the endpoint list page', () => {
|
|||
beforeEach(() => {
|
||||
const mockedContext = createAppRootMockRenderer();
|
||||
({ history, store, coreStart, middlewareSpy } = mockedContext);
|
||||
render = () => mockedContext.render(<EndpointList />);
|
||||
render = () => (renderResult = mockedContext.render(<EndpointList />));
|
||||
reactTestingLibrary.act(() => {
|
||||
history.push(`${MANAGEMENT_PATH}/endpoints`);
|
||||
});
|
||||
|
@ -186,9 +198,9 @@ describe('when on the endpoint list page', () => {
|
|||
endpointsResults: [],
|
||||
});
|
||||
|
||||
const renderResult = render();
|
||||
render();
|
||||
const timelineFlyout = renderResult.queryByTestId('timeline-bottom-bar-title-button');
|
||||
expect(timelineFlyout).toBeNull();
|
||||
expect(timelineFlyout).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when there are no endpoints or polices', () => {
|
||||
|
@ -199,47 +211,200 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should show the empty state when there are no hosts or polices', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding');
|
||||
});
|
||||
// Initially, there are no hosts or policies, so we prompt to add policies first.
|
||||
const table = await renderResult.findByTestId('emptyPolicyTable');
|
||||
expect(table).not.toBeNull();
|
||||
expect(table).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are policies, but no hosts', () => {
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
beforeEach(async () => {
|
||||
const policyData = mockPolicyResultList({ total: 3 }).items;
|
||||
const getOptionsTexts = async () => {
|
||||
const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect');
|
||||
const options = onboardingPolicySelect.querySelectorAll('[role=option]');
|
||||
|
||||
return [...options].map(({ textContent }) => textContent);
|
||||
};
|
||||
|
||||
const setupPolicyDataMocks = (
|
||||
partialPolicyData: PartialEndpointPolicyData[] = [
|
||||
{ name: 'Package 1', policy_ids: ['policy-1'] },
|
||||
]
|
||||
) => {
|
||||
const policyData = partialPolicyData.map((overrides) =>
|
||||
docGenerator.generatePolicyPackagePolicy({ overrides })
|
||||
);
|
||||
|
||||
setEndpointListApiMockImplementation(coreStart.http, {
|
||||
endpointsResults: [],
|
||||
endpointPackagePolicies: policyData,
|
||||
});
|
||||
};
|
||||
|
||||
renderResult = render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding');
|
||||
beforeEach(async () => {
|
||||
useBulkGetAgentPoliciesMock.mockReturnValue({
|
||||
data: [
|
||||
{ id: 'policy-1', name: 'Agent Policy 1' },
|
||||
{ id: 'policy-2', name: 'Agent Policy 2' },
|
||||
{ id: 'policy-3', name: 'Agent Policy 3' },
|
||||
],
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
setupPolicyDataMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should show the no hosts empty state', async () => {
|
||||
it('should show loading spinner while Agent Policies are loading', async () => {
|
||||
useBulkGetAgentPoliciesMock.mockReturnValue({ isLoading: true });
|
||||
render();
|
||||
expect(
|
||||
await renderResult.findByTestId('management-empty-state-loading-spinner')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the no hosts empty state without loading spinner', async () => {
|
||||
render();
|
||||
|
||||
expect(
|
||||
renderResult.queryByTestId('management-empty-state-loading-spinner')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable');
|
||||
expect(emptyHostsTable).not.toBeNull();
|
||||
expect(emptyHostsTable).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display the onboarding steps', async () => {
|
||||
render();
|
||||
const onboardingSteps = await renderResult.findByTestId('onboardingSteps');
|
||||
expect(onboardingSteps).not.toBeNull();
|
||||
expect(onboardingSteps).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show policy selection', async () => {
|
||||
const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect');
|
||||
expect(onboardingPolicySelect).not.toBeNull();
|
||||
describe('policy selection', () => {
|
||||
it('should show policy selection', async () => {
|
||||
render();
|
||||
const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect');
|
||||
expect(onboardingPolicySelect).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show discrete `package policy - agent policy` pairs', async () => {
|
||||
setupPolicyDataMocks([
|
||||
{ name: 'Package 1', policy_ids: ['policy-1'] },
|
||||
{ name: 'Package 2', policy_ids: ['policy-2'] },
|
||||
]);
|
||||
|
||||
render();
|
||||
const optionsTexts = await getOptionsTexts();
|
||||
|
||||
expect(optionsTexts).toStrictEqual([
|
||||
'Package 1 - Agent Policy 1',
|
||||
'Package 2 - Agent Policy 2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should display the same package policy with multiple Agent Policies multiple times', async () => {
|
||||
setupPolicyDataMocks([
|
||||
{ name: 'Package 1', policy_ids: ['policy-1', 'policy-2', 'policy-3'] },
|
||||
]);
|
||||
|
||||
render();
|
||||
const optionsTexts = await getOptionsTexts();
|
||||
|
||||
expect(optionsTexts).toStrictEqual([
|
||||
'Package 1 - Agent Policy 1',
|
||||
'Package 1 - Agent Policy 2',
|
||||
'Package 1 - Agent Policy 3',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not display a package policy without agent policy', async () => {
|
||||
setupPolicyDataMocks([
|
||||
{ name: 'Package 1', policy_ids: [] },
|
||||
{ name: 'Package 2', policy_ids: ['policy-1'] },
|
||||
]);
|
||||
|
||||
render();
|
||||
const optionsTexts = await getOptionsTexts();
|
||||
|
||||
expect(optionsTexts).toStrictEqual(['Package 2 - Agent Policy 1']);
|
||||
});
|
||||
|
||||
it("should fallback to agent policy ID if it's not found", async () => {
|
||||
setupPolicyDataMocks([{ name: 'Package 1', policy_ids: ['agent-policy-id'] }]);
|
||||
|
||||
render();
|
||||
const optionsTexts = await getOptionsTexts();
|
||||
expect(
|
||||
renderResult.queryByTestId('noIntegrationsAddedToAgentPoliciesCallout')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(optionsTexts).toStrictEqual(['Package 1 - agent-policy-id']);
|
||||
});
|
||||
|
||||
it('should show callout indicating that none of the integrations are added to agent policies', async () => {
|
||||
setupPolicyDataMocks([{ name: 'Package 1', policy_ids: [] }]);
|
||||
|
||||
render();
|
||||
|
||||
expect(
|
||||
await renderResult.findByTestId('noIntegrationsAddedToAgentPoliciesCallout')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration not added to agent policy callout', () => {
|
||||
it('should not display callout if all integrations are added to agent policies', async () => {
|
||||
setupPolicyDataMocks([
|
||||
{ name: 'Package 1', policy_ids: ['policy-1'] },
|
||||
{ name: 'Package 2', policy_ids: ['policy-2'] },
|
||||
]);
|
||||
|
||||
render();
|
||||
await getOptionsTexts();
|
||||
|
||||
expect(
|
||||
renderResult.queryByTestId('integrationsNotAddedToAgentPolicyCallout')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display callout if an integration is not added to an agent policy', async () => {
|
||||
setupPolicyDataMocks([
|
||||
{ name: 'Package 1', policy_ids: ['policy-1'] },
|
||||
{ name: 'Package 2', policy_ids: [] },
|
||||
]);
|
||||
|
||||
render();
|
||||
|
||||
expect(
|
||||
await renderResult.findByTestId('integrationsNotAddedToAgentPolicyCallout')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should list all integrations which are not added to an agent policy', async () => {
|
||||
setupPolicyDataMocks([
|
||||
{ name: 'Package 1', policy_ids: ['policy-1'] },
|
||||
{ name: 'Package 2', policy_ids: [] },
|
||||
{ name: 'Package 3', policy_ids: [] },
|
||||
{ name: 'Package 4', policy_ids: [] },
|
||||
]);
|
||||
|
||||
render();
|
||||
|
||||
const integrations = await renderResult.findAllByTestId(
|
||||
'integrationWithoutAgentPolicyListItem'
|
||||
);
|
||||
expect(integrations.map(({ textContent }) => textContent)).toStrictEqual([
|
||||
'Package 2',
|
||||
'Package 3',
|
||||
'Package 4',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -349,7 +514,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should display rows in the table', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
|
@ -357,7 +522,7 @@ describe('when on the endpoint list page', () => {
|
|||
expect(rows).toHaveLength(6);
|
||||
});
|
||||
it('should show total', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
|
@ -365,7 +530,7 @@ describe('when on the endpoint list page', () => {
|
|||
expect(total.textContent).toEqual('Showing 5 endpoints');
|
||||
});
|
||||
it('should agent status', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
|
@ -380,7 +545,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should display correct policy status', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
|
@ -394,12 +559,12 @@ describe('when on the endpoint list page', () => {
|
|||
POLICY_STATUS_TO_HEALTH_COLOR[generatedPolicyStatuses[index]]
|
||||
}]`
|
||||
)
|
||||
).not.toBeNull();
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display policy out-of-date warning when changes pending', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
|
@ -412,12 +577,12 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should display policy name as a link', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink-link'))[0];
|
||||
expect(firstPolicyName).not.toBeNull();
|
||||
expect(firstPolicyName).toBeInTheDocument();
|
||||
expect(firstPolicyName.getAttribute('href')).toEqual(
|
||||
`${APP_PATH}${MANAGEMENT_PATH}/policy/${firstPolicyID}/settings`
|
||||
);
|
||||
|
@ -425,7 +590,6 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
describe('when the user clicks the first hostname in the table', () => {
|
||||
const endpointDetails: HostInfo = mockEndpointDetailsApiResult();
|
||||
let renderResult: reactTestingLibrary.RenderResult;
|
||||
beforeEach(async () => {
|
||||
mockUseGetEndpointDetails.mockReturnValue({
|
||||
data: {
|
||||
|
@ -447,7 +611,7 @@ describe('when on the endpoint list page', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
|
@ -459,20 +623,20 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
it('should show the flyout', async () => {
|
||||
return renderResult.findByTestId('endpointDetailsFlyout').then((flyout) => {
|
||||
expect(flyout).not.toBeNull();
|
||||
expect(flyout).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show revision number', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
const firstPolicyRevElement = (
|
||||
await renderResult.findAllByTestId('policyNameCellLink-revision')
|
||||
)[0];
|
||||
expect(firstPolicyRevElement).not.toBeNull();
|
||||
expect(firstPolicyRevElement).toBeInTheDocument();
|
||||
expect(firstPolicyRevElement.textContent).toEqual(`rev. ${firstPolicyRev}`);
|
||||
});
|
||||
});
|
||||
|
@ -502,7 +666,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should update data after some time', async () => {
|
||||
let renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
|
@ -518,7 +682,7 @@ describe('when on the endpoint list page', () => {
|
|||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
|
||||
renderResult = render();
|
||||
render();
|
||||
|
||||
const updatedTotal = await renderResult.findAllByTestId('endpointListTableTotal');
|
||||
expect(updatedTotal[0].textContent).toEqual('1 Host');
|
||||
|
@ -601,33 +765,33 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should show the flyout and footer', async () => {
|
||||
const renderResult = render();
|
||||
expect(renderResult.getByTestId('endpointDetailsFlyout')).not.toBeNull();
|
||||
expect(renderResult.getByTestId('endpointDetailsFlyoutFooter')).not.toBeNull();
|
||||
render();
|
||||
expect(renderResult.getByTestId('endpointDetailsFlyout')).toBeInTheDocument();
|
||||
expect(renderResult.getByTestId('endpointDetailsFlyoutFooter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display policy name value as a link', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
const policyDetailsLink = await renderResult.findByTestId('policyNameCellLink-link');
|
||||
expect(policyDetailsLink).not.toBeNull();
|
||||
expect(policyDetailsLink).toBeInTheDocument();
|
||||
expect(policyDetailsLink.getAttribute('href')).toEqual(
|
||||
`${APP_PATH}${MANAGEMENT_PATH}/policy/${hostInfo.metadata.Endpoint.policy.applied.id}/settings`
|
||||
);
|
||||
});
|
||||
|
||||
it('should display policy revision number', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
const policyDetailsRevElement = await renderResult.findByTestId(
|
||||
'policyNameCellLink-revision'
|
||||
);
|
||||
expect(policyDetailsRevElement).not.toBeNull();
|
||||
expect(policyDetailsRevElement).toBeInTheDocument();
|
||||
expect(policyDetailsRevElement.textContent).toEqual(
|
||||
`rev. ${hostInfo.metadata.Endpoint.policy.applied.endpoint_policy_version}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should update the URL when policy name link is clicked', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
const policyDetailsLink = await renderResult.findByTestId('policyNameCellLink-link');
|
||||
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
|
||||
reactTestingLibrary.act(() => {
|
||||
|
@ -640,7 +804,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should update the URL when policy status link is clicked', async () => {
|
||||
const renderResult = render();
|
||||
render();
|
||||
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
|
||||
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
|
||||
reactTestingLibrary.act(() => {
|
||||
|
@ -654,7 +818,7 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
it('should display Success overall policy status', async () => {
|
||||
getMockUseEndpointDetails(HostPolicyResponseActionStatus.success);
|
||||
const renderResult = render();
|
||||
render();
|
||||
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
|
||||
expect(renderResult.getByTestId('policyStatusValue-success')).toBeTruthy();
|
||||
expect(policyStatusBadge.textContent).toEqual('Success');
|
||||
|
@ -662,7 +826,7 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
it('should display Warning overall policy status', async () => {
|
||||
getMockUseEndpointDetails(HostPolicyResponseActionStatus.warning);
|
||||
const renderResult = render();
|
||||
render();
|
||||
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
|
||||
expect(policyStatusBadge.textContent).toEqual('Warning');
|
||||
expect(renderResult.getByTestId('policyStatusValue-warning')).toBeTruthy();
|
||||
|
@ -670,7 +834,7 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
it('should display Failed overall policy status', async () => {
|
||||
getMockUseEndpointDetails(HostPolicyResponseActionStatus.failure);
|
||||
const renderResult = render();
|
||||
render();
|
||||
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
|
||||
expect(policyStatusBadge.textContent).toEqual('Failed');
|
||||
expect(renderResult.getByTestId('policyStatusValue-failure')).toBeTruthy();
|
||||
|
@ -678,15 +842,15 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
it('should display Unknown overall policy status', async () => {
|
||||
getMockUseEndpointDetails('' as HostPolicyResponseActionStatus);
|
||||
const renderResult = render();
|
||||
render();
|
||||
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
|
||||
expect(policyStatusBadge.textContent).toEqual('Unknown');
|
||||
expect(renderResult.getByTestId('policyStatusValue-')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show the Take Action button', async () => {
|
||||
const renderResult = render();
|
||||
expect(renderResult.getByTestId('endpointDetailsActionsButton')).not.toBeNull();
|
||||
render();
|
||||
expect(renderResult.getByTestId('endpointDetailsActionsButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Activity Log tab', () => {
|
||||
|
@ -705,8 +869,8 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
describe('when `canReadActionsLogManagement` is TRUE', () => {
|
||||
it('should start with the activity log tab as unselected', async () => {
|
||||
const renderResult = await render();
|
||||
it('should start with the activity log tab as unselected', () => {
|
||||
render();
|
||||
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
|
||||
const activityLogTab = renderResult.getByTestId(
|
||||
'endpoint-details-flyout-tab-activity_log'
|
||||
|
@ -714,12 +878,14 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
expect(detailsTab).toHaveAttribute('aria-selected', 'true');
|
||||
expect(activityLogTab).toHaveAttribute('aria-selected', 'false');
|
||||
expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).not.toBeNull();
|
||||
expect(renderResult.queryByTestId('endpointActivityLogFlyoutBody')).toBeNull();
|
||||
expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).toBeInTheDocument();
|
||||
expect(
|
||||
renderResult.queryByTestId('endpointActivityLogFlyoutBody')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the activity log content when selected', async () => {
|
||||
const renderResult = await render();
|
||||
render();
|
||||
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
|
||||
const activityLogTab = renderResult.getByTestId(
|
||||
'endpoint-details-flyout-tab-activity_log'
|
||||
|
@ -728,13 +894,13 @@ describe('when on the endpoint list page', () => {
|
|||
await userEvent.click(activityLogTab);
|
||||
expect(detailsTab).toHaveAttribute('aria-selected', 'false');
|
||||
expect(activityLogTab).toHaveAttribute('aria-selected', 'true');
|
||||
expect(renderResult.getByTestId('endpointActivityLogFlyoutBody')).not.toBeNull();
|
||||
expect(renderResult.queryByTestId('endpointDetailsFlyoutBody')).toBeNull();
|
||||
expect(renderResult.getByTestId('endpointActivityLogFlyoutBody')).toBeInTheDocument();
|
||||
expect(renderResult.queryByTestId('endpointDetailsFlyoutBody')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `canReadActionsLogManagement` is FALSE', () => {
|
||||
it('should not show the response actions history tab', async () => {
|
||||
it('should not show the response actions history tab', () => {
|
||||
mockUserPrivileges.mockReturnValue({
|
||||
...mockInitialUserPrivilegesState(),
|
||||
endpointPrivileges: {
|
||||
|
@ -744,15 +910,15 @@ describe('when on the endpoint list page', () => {
|
|||
canAccessFleet: true,
|
||||
},
|
||||
});
|
||||
const renderResult = await render();
|
||||
render();
|
||||
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
|
||||
const activityLogTab = renderResult.queryByTestId(
|
||||
'endpoint-details-flyout-tab-activity_log'
|
||||
);
|
||||
|
||||
expect(detailsTab).toHaveAttribute('aria-selected', 'true');
|
||||
expect(activityLogTab).toBeNull();
|
||||
expect(renderResult.findByTestId('endpointDetailsFlyoutBody')).not.toBeNull();
|
||||
expect(activityLogTab).not.toBeInTheDocument();
|
||||
expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the overview tab when force loading actions history tab via URL', async () => {
|
||||
|
@ -769,7 +935,7 @@ describe('when on the endpoint list page', () => {
|
|||
history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1&show=activity_log`);
|
||||
});
|
||||
|
||||
const renderResult = await render();
|
||||
render();
|
||||
await middlewareSpy.waitForAction('serverFinishedInitialization');
|
||||
|
||||
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
|
||||
|
@ -778,14 +944,13 @@ describe('when on the endpoint list page', () => {
|
|||
);
|
||||
|
||||
expect(detailsTab).toHaveAttribute('aria-selected', 'true');
|
||||
expect(activityLogTab).toBeNull();
|
||||
expect(renderResult.findByTestId('endpointDetailsFlyoutBody')).not.toBeNull();
|
||||
expect(activityLogTab).not.toBeInTheDocument();
|
||||
expect(renderResult.getByTestId('endpointDetailsFlyoutBody')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when showing host Policy Response panel', () => {
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
beforeEach(async () => {
|
||||
coreStart.http.post.mockImplementation(async (requestOptions) => {
|
||||
if (requestOptions.path === HOST_METADATA_LIST_ROUTE) {
|
||||
|
@ -793,7 +958,7 @@ describe('when on the endpoint list page', () => {
|
|||
}
|
||||
throw new Error(`POST to '${requestOptions.path}' does not have a mock response!`);
|
||||
});
|
||||
renderResult = await render();
|
||||
render();
|
||||
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
|
||||
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
|
||||
reactTestingLibrary.act(() => {
|
||||
|
@ -806,14 +971,14 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
it('should hide the host details panel', async () => {
|
||||
const endpointDetailsFlyout = renderResult.queryByTestId('endpointDetailsFlyoutBody');
|
||||
expect(endpointDetailsFlyout).toBeNull();
|
||||
expect(endpointDetailsFlyout).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display policy response sub-panel', async () => {
|
||||
expect(await renderResult.findByTestId('flyoutSubHeaderBackButton')).not.toBeNull();
|
||||
expect(await renderResult.findByTestId('flyoutSubHeaderBackButton')).toBeInTheDocument();
|
||||
expect(
|
||||
await renderResult.findByTestId('endpointDetailsPolicyResponseFlyoutBody')
|
||||
).not.toBeNull();
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should include the back to details link', async () => {
|
||||
|
@ -862,14 +1027,13 @@ describe('when on the endpoint list page', () => {
|
|||
};
|
||||
|
||||
let isolateApiMock: ReturnType<typeof hostIsolationHttpMocks>;
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
|
||||
beforeEach(async () => {
|
||||
getKibanaServicesMock.mockReturnValue(coreStart);
|
||||
reactTestingLibrary.act(() => {
|
||||
history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1&show=isolate`);
|
||||
});
|
||||
renderResult = render();
|
||||
render();
|
||||
await middlewareSpy.waitForAction('serverFinishedInitialization');
|
||||
|
||||
// Need to reset `http.post` and adjust it so that the mock for http host
|
||||
|
@ -880,7 +1044,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should show the isolate form', () => {
|
||||
expect(renderResult.getByTestId('host_isolation_comment')).not.toBeNull();
|
||||
expect(renderResult.getByTestId('host_isolation_comment')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should take you back to details when back link below the flyout header is clicked', async () => {
|
||||
|
@ -922,7 +1086,7 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
it('should isolate endpoint host when confirm is clicked', async () => {
|
||||
await confirmIsolateAndWaitForApiResponse();
|
||||
expect(renderResult.getByTestId('hostIsolateSuccessMessage')).not.toBeNull();
|
||||
expect(renderResult.getByTestId('hostIsolateSuccessMessage')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigate to details when the Complete button on success message is clicked', async () => {
|
||||
|
@ -946,7 +1110,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
await confirmIsolateAndWaitForApiResponse('failure');
|
||||
|
||||
expect(renderResult.getByText('oh oh. something went wrong')).not.toBeNull();
|
||||
expect(renderResult.getByText('oh oh. something went wrong')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should reset isolation state and show form again', async () => {
|
||||
|
@ -954,7 +1118,7 @@ describe('when on the endpoint list page', () => {
|
|||
// (`show` is NOT `isolate`), then the state should be reset so that the form show up again the next
|
||||
// time `isolate host` is clicked
|
||||
await confirmIsolateAndWaitForApiResponse();
|
||||
expect(renderResult.getByTestId('hostIsolateSuccessMessage')).not.toBeNull();
|
||||
expect(renderResult.getByTestId('hostIsolateSuccessMessage')).toBeInTheDocument();
|
||||
|
||||
// Close flyout
|
||||
const changeUrlAction = middlewareSpy.waitForAction('userChangedUrl');
|
||||
|
@ -975,7 +1139,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should NOT show the flyout footer', () => {
|
||||
expect(renderResult.queryByTestId('endpointDetailsFlyoutFooter')).toBeNull();
|
||||
expect(renderResult.queryByTestId('endpointDetailsFlyoutFooter')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -985,7 +1149,6 @@ describe('when on the endpoint list page', () => {
|
|||
let hostInfo: HostInfo[];
|
||||
let agentId: string;
|
||||
let agentPolicyId: string;
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
let endpointActionsButton: HTMLElement;
|
||||
|
||||
// 2nd endpoint only has isolation capabilities
|
||||
|
@ -1069,7 +1232,7 @@ describe('when on the endpoint list page', () => {
|
|||
history.push(`${MANAGEMENT_PATH}/endpoints`);
|
||||
});
|
||||
|
||||
renderResult = render();
|
||||
render();
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies');
|
||||
|
||||
|
@ -1130,7 +1293,7 @@ describe('when on the endpoint list page', () => {
|
|||
reactTestingLibrary.fireEvent.click(endpointActionsButton);
|
||||
});
|
||||
const isolateLink = screen.queryByTestId('isolateLink');
|
||||
expect(isolateLink).toBeNull();
|
||||
expect(isolateLink).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to the Security Solution Host Details page', async () => {
|
||||
|
@ -1179,7 +1342,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
render();
|
||||
const banner = screen.queryByTestId('callout-endpoints-list-transform-failed');
|
||||
expect(banner).toBeNull();
|
||||
expect(banner).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is not displayed when non-relevant transform is failing', () => {
|
||||
|
@ -1193,7 +1356,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
render();
|
||||
const banner = screen.queryByTestId('callout-endpoints-list-transform-failed');
|
||||
expect(banner).toBeNull();
|
||||
expect(banner).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is not displayed when no endpoint policy', () => {
|
||||
|
@ -1207,7 +1370,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
render();
|
||||
const banner = screen.queryByTestId('callout-endpoints-list-transform-failed');
|
||||
expect(banner).toBeNull();
|
||||
expect(banner).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is displayed when relevant transform state is failed state', async () => {
|
||||
|
@ -1268,12 +1431,12 @@ describe('when on the endpoint list page', () => {
|
|||
canAccessFleet: true,
|
||||
}),
|
||||
});
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding');
|
||||
});
|
||||
const onboardingSteps = await renderResult.findByTestId('onboardingSteps');
|
||||
expect(onboardingSteps).not.toBeNull();
|
||||
expect(onboardingSteps).toBeInTheDocument();
|
||||
});
|
||||
it('user has endpoint list READ and fleet All and can view entire onboarding screen', async () => {
|
||||
mockUserPrivileges.mockReturnValue({
|
||||
|
@ -1283,12 +1446,12 @@ describe('when on the endpoint list page', () => {
|
|||
canAccessFleet: true,
|
||||
}),
|
||||
});
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding');
|
||||
});
|
||||
const onboardingSteps = await renderResult.findByTestId('onboardingSteps');
|
||||
expect(onboardingSteps).not.toBeNull();
|
||||
expect(onboardingSteps).toBeInTheDocument();
|
||||
});
|
||||
it('user has endpoint list ALL/READ and fleet NONE and can view a modified onboarding screen with no actions link to fleet', async () => {
|
||||
mockUserPrivileges.mockReturnValue({
|
||||
|
@ -1298,28 +1461,26 @@ describe('when on the endpoint list page', () => {
|
|||
canAccessFleet: false,
|
||||
}),
|
||||
});
|
||||
const renderResult = render();
|
||||
render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding');
|
||||
});
|
||||
const onboardingSteps = await renderResult.findByTestId('policyOnboardingInstructions');
|
||||
expect(onboardingSteps).not.toBeNull();
|
||||
expect(onboardingSteps).toBeInTheDocument();
|
||||
const noPrivilegesPage = await renderResult.findByTestId('noFleetAccess');
|
||||
expect(noPrivilegesPage).not.toBeNull();
|
||||
expect(noPrivilegesPage).toBeInTheDocument();
|
||||
const startButton = renderResult.queryByTestId('onboardingStartButton');
|
||||
expect(startButton).toBeNull();
|
||||
expect(startButton).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('endpoint list take action with RBAC controls', () => {
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
|
||||
const renderAndClickActionsButton = async (tableRow: number = 0) => {
|
||||
reactTestingLibrary.act(() => {
|
||||
history.push(`${MANAGEMENT_PATH}/endpoints`);
|
||||
});
|
||||
|
||||
renderResult = render();
|
||||
render();
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies');
|
||||
|
||||
|
@ -1408,7 +1569,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
await renderAndClickActionsButton();
|
||||
const isolateLink = await renderResult.findByTestId('isolateLink');
|
||||
expect(isolateLink).not.toBeNull();
|
||||
expect(isolateLink).toBeInTheDocument();
|
||||
});
|
||||
it('hides Isolate host option if canIsolateHost is NONE', async () => {
|
||||
mockUserPrivileges.mockReturnValue({
|
||||
|
@ -1420,7 +1581,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
await renderAndClickActionsButton();
|
||||
const isolateLink = screen.queryByTestId('isolateLink');
|
||||
expect(isolateLink).toBeNull();
|
||||
expect(isolateLink).not.toBeInTheDocument();
|
||||
});
|
||||
it('shows unisolate host option if canUnHostIsolate is READ/ALL', async () => {
|
||||
mockUserPrivileges.mockReturnValue({
|
||||
|
@ -1432,7 +1593,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
await renderAndClickActionsButton(1);
|
||||
const unisolateLink = await renderResult.findByTestId('unIsolateLink');
|
||||
expect(unisolateLink).not.toBeNull();
|
||||
expect(unisolateLink).toBeInTheDocument();
|
||||
});
|
||||
it('hides unisolate host option if canUnIsolateHost is NONE', async () => {
|
||||
mockUserPrivileges.mockReturnValue({
|
||||
|
@ -1444,7 +1605,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
await renderAndClickActionsButton(1);
|
||||
const unisolateLink = renderResult.queryByTestId('unIsolateLink');
|
||||
expect(unisolateLink).toBeNull();
|
||||
expect(unisolateLink).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the Responder option when at least one rbac privilege from host isolation, process operation and file operation, is set to TRUE', async () => {
|
||||
|
@ -1457,7 +1618,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
await renderAndClickActionsButton();
|
||||
const responderButton = await renderResult.findByTestId('console');
|
||||
expect(responderButton).not.toBeNull();
|
||||
expect(responderButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides the Responder option when host isolation, process operation and file operations are ALL set to NONE', async () => {
|
||||
|
@ -1470,13 +1631,13 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
await renderAndClickActionsButton();
|
||||
const responderButton = renderResult.queryByTestId('console');
|
||||
expect(responderButton).toBeNull();
|
||||
expect(responderButton).not.toBeInTheDocument();
|
||||
});
|
||||
it('always shows the Host details link', async () => {
|
||||
mockUserPrivileges.mockReturnValue(getUserPrivilegesMockDefaultValue());
|
||||
await renderAndClickActionsButton();
|
||||
const hostLink = await renderResult.findByTestId('hostLink');
|
||||
expect(hostLink).not.toBeNull();
|
||||
expect(hostLink).toBeInTheDocument();
|
||||
});
|
||||
it('shows Agent Policy, View Agent Details and Reassign Policy Links when canReadFleetAgents,canWriteFleetAgents,canReadFleetAgentPolicies RBAC control is enabled', async () => {
|
||||
mockUserPrivileges.mockReturnValue({
|
||||
|
@ -1493,9 +1654,9 @@ describe('when on the endpoint list page', () => {
|
|||
const agentPolicyLink = await renderResult.findByTestId('agentPolicyLink');
|
||||
const agentDetailsLink = await renderResult.findByTestId('agentDetailsLink');
|
||||
const agentPolicyReassignLink = await renderResult.findByTestId('agentPolicyReassignLink');
|
||||
expect(agentPolicyLink).not.toBeNull();
|
||||
expect(agentDetailsLink).not.toBeNull();
|
||||
expect(agentPolicyReassignLink).not.toBeNull();
|
||||
expect(agentPolicyLink).toBeInTheDocument();
|
||||
expect(agentDetailsLink).toBeInTheDocument();
|
||||
expect(agentPolicyReassignLink).toBeInTheDocument();
|
||||
});
|
||||
it('hides Agent Policy, View Agent Details and Reassign Policy Links when canAccessFleet RBAC control is NOT enabled', async () => {
|
||||
mockUserPrivileges.mockReturnValue({
|
||||
|
@ -1509,9 +1670,9 @@ describe('when on the endpoint list page', () => {
|
|||
const agentPolicyLink = renderResult.queryByTestId('agentPolicyLink');
|
||||
const agentDetailsLink = renderResult.queryByTestId('agentDetailsLink');
|
||||
const agentPolicyReassignLink = renderResult.queryByTestId('agentPolicyReassignLink');
|
||||
expect(agentPolicyLink).toBeNull();
|
||||
expect(agentDetailsLink).toBeNull();
|
||||
expect(agentPolicyReassignLink).toBeNull();
|
||||
expect(agentPolicyLink).not.toBeInTheDocument();
|
||||
expect(agentDetailsLink).not.toBeInTheDocument();
|
||||
expect(agentPolicyReassignLink).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,7 +40,7 @@ import { EndpointListNavLink } from './components/endpoint_list_nav_link';
|
|||
import { AgentStatus } from '../../../../common/components/endpoint/agents/agent_status';
|
||||
import { EndpointDetailsFlyout } from './details';
|
||||
import * as selectors from '../store/selectors';
|
||||
import { nonExistingPolicies } from '../store/selectors';
|
||||
import type { nonExistingPolicies } from '../store/selectors';
|
||||
import { useEndpointSelector } from './hooks';
|
||||
import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants';
|
||||
import type { CreateStructuredSelector } from '../../../../common/store';
|
||||
|
@ -69,6 +69,7 @@ import { APP_UI_ID } from '../../../../../common/constants';
|
|||
import { ManagementEmptyStateWrapper } from '../../../components/management_empty_state_wrapper';
|
||||
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||
import { BackToPolicyListButton } from './components/back_to_policy_list_button';
|
||||
import { useBulkGetAgentPolicies } from '../../../services/policies/hooks';
|
||||
|
||||
const MAX_PAGINATED_ITEM = 9999;
|
||||
|
||||
|
@ -338,8 +339,8 @@ export const EndpointList = () => {
|
|||
patternsError,
|
||||
metadataTransformStats,
|
||||
isInitialized,
|
||||
nonExistingPolicies: missingPolicies,
|
||||
} = useEndpointSelector(selector);
|
||||
const missingPolicies = useEndpointSelector(nonExistingPolicies);
|
||||
const {
|
||||
canReadEndpointList,
|
||||
canAccessFleet,
|
||||
|
@ -353,24 +354,22 @@ export const EndpointList = () => {
|
|||
// cap ability to page at 10k records. (max_result_window)
|
||||
const maxPageCount = totalItemCount > MAX_PAGINATED_ITEM ? MAX_PAGINATED_ITEM : totalItemCount;
|
||||
|
||||
const hasPolicyData = useMemo(() => policyItems && policyItems.length > 0, [policyItems]);
|
||||
const hasListData = useMemo(() => listData && listData.length > 0, [listData]);
|
||||
const hasPolicyData = policyItems && policyItems.length > 0;
|
||||
const hasListData = listData && listData.length > 0;
|
||||
|
||||
const refreshStyle = useMemo(() => {
|
||||
return { display: endpointsExist ? 'flex' : 'none', maxWidth: 200 };
|
||||
}, [endpointsExist]);
|
||||
|
||||
const refreshIsPaused = useMemo(() => {
|
||||
return !endpointsExist ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled;
|
||||
}, [endpointsExist, hasSelectedEndpoint, isAutoRefreshEnabled]);
|
||||
const refreshIsPaused = !endpointsExist
|
||||
? false
|
||||
: hasSelectedEndpoint
|
||||
? true
|
||||
: !isAutoRefreshEnabled;
|
||||
|
||||
const refreshInterval = useMemo(() => {
|
||||
return !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval;
|
||||
}, [endpointsExist, autoRefreshInterval]);
|
||||
const refreshInterval = !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval;
|
||||
|
||||
const shouldShowKQLBar = useMemo(() => {
|
||||
return endpointsExist && !patternsError;
|
||||
}, [endpointsExist, patternsError]);
|
||||
const shouldShowKQLBar = endpointsExist && !patternsError;
|
||||
|
||||
const paginationSetup = useMemo(() => {
|
||||
return {
|
||||
|
@ -465,6 +464,57 @@ export const EndpointList = () => {
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
const stateToDisplay:
|
||||
| 'loading'
|
||||
| 'policyEmptyState'
|
||||
| 'policyEmptyStateWithoutFleetAccess'
|
||||
| 'hostsEmptyState'
|
||||
| 'endpointTable'
|
||||
| 'listError' = useMemo(() => {
|
||||
if (!isInitialized) {
|
||||
return 'loading';
|
||||
} else if (listError) {
|
||||
return 'listError';
|
||||
} else if (endpointsExist) {
|
||||
return 'endpointTable';
|
||||
} else if (canReadEndpointList && !canAccessFleet) {
|
||||
return 'policyEmptyStateWithoutFleetAccess';
|
||||
} else if (!policyItemsLoading && hasPolicyData) {
|
||||
return 'hostsEmptyState';
|
||||
} else {
|
||||
return 'policyEmptyState';
|
||||
}
|
||||
}, [
|
||||
canAccessFleet,
|
||||
canReadEndpointList,
|
||||
endpointsExist,
|
||||
hasPolicyData,
|
||||
isInitialized,
|
||||
listError,
|
||||
policyItemsLoading,
|
||||
]);
|
||||
|
||||
const referencedAgentPolicyIds: string[] = useMemo(
|
||||
// Agent Policy IDs should be unique as one Agent Policy can have only one Defend integration
|
||||
() => policyItems.flatMap((item) => item.policy_ids),
|
||||
[policyItems]
|
||||
);
|
||||
|
||||
const { data: referencedAgentPolicies, isLoading: isAgentPolicesLoading } =
|
||||
useBulkGetAgentPolicies({
|
||||
isEnabled: stateToDisplay === 'hostsEmptyState',
|
||||
policyIds: referencedAgentPolicyIds,
|
||||
});
|
||||
|
||||
const agentPolicyNameMap = useMemo(
|
||||
() =>
|
||||
referencedAgentPolicies?.reduce<Record<string, string>>((acc, policy) => {
|
||||
acc[policy.id] = policy.name;
|
||||
return acc;
|
||||
}, {}) ?? {},
|
||||
[referencedAgentPolicies]
|
||||
);
|
||||
|
||||
// Used for an auto-refresh super date picker version without any date/time selection
|
||||
const onTimeChange = useCallback(() => {}, []);
|
||||
|
||||
|
@ -526,86 +576,92 @@ export const EndpointList = () => {
|
|||
);
|
||||
|
||||
const mutableListData = useMemo(() => [...listData], [listData]);
|
||||
|
||||
const renderTableOrEmptyState = useMemo(() => {
|
||||
if (!isInitialized) {
|
||||
return (
|
||||
<ManagementEmptyStateWrapper>
|
||||
<EuiEmptyPrompt
|
||||
icon={<EuiLoadingLogo logo="logoSecurity" size="xl" />}
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('xpack.securitySolution.endpoint.list.loadingEndpointManagement', {
|
||||
defaultMessage: 'Loading Endpoint Management',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
switch (stateToDisplay) {
|
||||
case 'loading':
|
||||
return (
|
||||
<ManagementEmptyStateWrapper>
|
||||
<EuiEmptyPrompt
|
||||
icon={<EuiLoadingLogo logo="logoSecurity" size="xl" />}
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.securitySolution.endpoint.list.loadingEndpointManagement',
|
||||
{
|
||||
defaultMessage: 'Loading Endpoint Management',
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
}
|
||||
/>
|
||||
</ManagementEmptyStateWrapper>
|
||||
);
|
||||
case 'listError':
|
||||
return (
|
||||
<ManagementEmptyStateWrapper>
|
||||
<EuiEmptyPrompt
|
||||
color="danger"
|
||||
iconType="error"
|
||||
title={<h2>{listError?.error}</h2>}
|
||||
body={<p>{listError?.message}</p>}
|
||||
/>
|
||||
</ManagementEmptyStateWrapper>
|
||||
);
|
||||
case 'endpointTable':
|
||||
return (
|
||||
<EuiBasicTable
|
||||
data-test-subj="endpointListTable"
|
||||
items={mutableListData}
|
||||
columns={columns}
|
||||
pagination={paginationSetup}
|
||||
onChange={onTableChange}
|
||||
loading={loading}
|
||||
rowProps={setTableRowProps}
|
||||
sorting={sorting}
|
||||
/>
|
||||
</ManagementEmptyStateWrapper>
|
||||
);
|
||||
} else if (listError) {
|
||||
return (
|
||||
<ManagementEmptyStateWrapper>
|
||||
<EuiEmptyPrompt
|
||||
color="danger"
|
||||
iconType="error"
|
||||
title={<h2>{listError.error}</h2>}
|
||||
body={<p>{listError.message}</p>}
|
||||
);
|
||||
case 'policyEmptyStateWithoutFleetAccess':
|
||||
return (
|
||||
<ManagementEmptyStateWrapper>
|
||||
<PolicyEmptyState loading={endpointPrivilegesLoading} />
|
||||
</ManagementEmptyStateWrapper>
|
||||
);
|
||||
case 'hostsEmptyState':
|
||||
const selectionOptions: EuiSelectableProps['options'] = policyItems.flatMap((policy) =>
|
||||
// displaying Package Policy - Agent Policy pairs
|
||||
policy.policy_ids.map((agentPolicyId) => ({
|
||||
key: agentPolicyId,
|
||||
label: `${policy.name} - ${agentPolicyNameMap[agentPolicyId] || agentPolicyId}`,
|
||||
checked: selectedPolicyId === agentPolicyId ? 'on' : undefined,
|
||||
}))
|
||||
);
|
||||
|
||||
return (
|
||||
<HostsEmptyState
|
||||
loading={loading || isAgentPolicesLoading}
|
||||
onActionClick={handleDeployEndpointsClick}
|
||||
actionDisabled={!selectedPolicyId}
|
||||
handleSelectableOnChange={handleSelectableOnChange}
|
||||
selectionOptions={selectionOptions}
|
||||
policyItems={policyItems}
|
||||
/>
|
||||
</ManagementEmptyStateWrapper>
|
||||
);
|
||||
} else if (endpointsExist) {
|
||||
return (
|
||||
<EuiBasicTable
|
||||
data-test-subj="endpointListTable"
|
||||
items={mutableListData}
|
||||
columns={columns}
|
||||
pagination={paginationSetup}
|
||||
onChange={onTableChange}
|
||||
loading={loading}
|
||||
rowProps={setTableRowProps}
|
||||
sorting={sorting}
|
||||
/>
|
||||
);
|
||||
} else if (canReadEndpointList && !canAccessFleet) {
|
||||
return (
|
||||
<ManagementEmptyStateWrapper>
|
||||
<PolicyEmptyState loading={endpointPrivilegesLoading} />
|
||||
</ManagementEmptyStateWrapper>
|
||||
);
|
||||
} else if (!policyItemsLoading && hasPolicyData) {
|
||||
const selectionOptions: EuiSelectableProps['options'] = policyItems
|
||||
.filter((item) => item.policy_id)
|
||||
.map((item) => {
|
||||
return {
|
||||
key: item.policy_id as string,
|
||||
label: item.name,
|
||||
checked: selectedPolicyId === item.policy_id ? 'on' : undefined,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<HostsEmptyState
|
||||
loading={loading}
|
||||
onActionClick={handleDeployEndpointsClick}
|
||||
actionDisabled={!selectedPolicyId}
|
||||
handleSelectableOnChange={handleSelectableOnChange}
|
||||
selectionOptions={selectionOptions}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ManagementEmptyStateWrapper>
|
||||
<PolicyEmptyState loading={policyItemsLoading} onActionClick={handleCreatePolicyClick} />
|
||||
</ManagementEmptyStateWrapper>
|
||||
);
|
||||
);
|
||||
case 'policyEmptyState':
|
||||
default:
|
||||
return (
|
||||
<ManagementEmptyStateWrapper>
|
||||
<PolicyEmptyState
|
||||
loading={policyItemsLoading}
|
||||
onActionClick={handleCreatePolicyClick}
|
||||
/>
|
||||
</ManagementEmptyStateWrapper>
|
||||
);
|
||||
}
|
||||
}, [
|
||||
isInitialized,
|
||||
stateToDisplay,
|
||||
listError,
|
||||
endpointsExist,
|
||||
canReadEndpointList,
|
||||
canAccessFleet,
|
||||
policyItemsLoading,
|
||||
hasPolicyData,
|
||||
mutableListData,
|
||||
columns,
|
||||
paginationSetup,
|
||||
|
@ -615,6 +671,8 @@ export const EndpointList = () => {
|
|||
sorting,
|
||||
endpointPrivilegesLoading,
|
||||
policyItems,
|
||||
agentPolicyNameMap,
|
||||
isAgentPolicesLoading,
|
||||
handleDeployEndpointsClick,
|
||||
selectedPolicyId,
|
||||
handleSelectableOnChange,
|
||||
|
|
|
@ -7,13 +7,14 @@
|
|||
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { GetInfoResponse } from '@kbn/fleet-plugin/common';
|
||||
import type { BulkGetAgentPoliciesResponse } from '@kbn/fleet-plugin/common';
|
||||
import { type GetInfoResponse } from '@kbn/fleet-plugin/common';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import type { IKibanaSearchResponse } from '@kbn/search-types';
|
||||
import { ENDPOINT_PACKAGE_POLICIES_STATS_STRATEGY } from '../../../../common/endpoint/constants';
|
||||
import { useHttp, useKibana } from '../../../common/lib/kibana';
|
||||
import { MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../common/constants';
|
||||
import { sendGetEndpointSecurityPackage } from './ingest';
|
||||
import { sendBulkGetAgentPolicies, sendGetEndpointSecurityPackage } from './ingest';
|
||||
import type { GetPolicyListResponse } from '../../pages/policy/types';
|
||||
import { sendGetEndpointSpecificPackagePolicies } from './policies';
|
||||
import type { ServerApiError } from '../../../common/types';
|
||||
|
@ -83,3 +84,23 @@ export function useGetEndpointSecurityPackage({
|
|||
customQueryOptions
|
||||
);
|
||||
}
|
||||
|
||||
export function useBulkGetAgentPolicies({
|
||||
isEnabled,
|
||||
policyIds,
|
||||
}: {
|
||||
isEnabled: boolean;
|
||||
policyIds: string[];
|
||||
}): QueryObserverResult<BulkGetAgentPoliciesResponse['items'], IHttpFetchError> {
|
||||
const http = useHttp();
|
||||
|
||||
return useQuery<BulkGetAgentPoliciesResponse['items'], IHttpFetchError>(
|
||||
['agentPolicies', policyIds],
|
||||
|
||||
async () => {
|
||||
return (await sendBulkGetAgentPolicies({ http, requestBody: { ids: policyIds } }))?.items;
|
||||
},
|
||||
|
||||
{ enabled: isEnabled, refetchOnWindowFocus: false, retry: 1 }
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,9 +10,12 @@ import type {
|
|||
GetAgentStatusResponse,
|
||||
GetPackagePoliciesResponse,
|
||||
GetInfoResponse,
|
||||
BulkGetAgentPoliciesResponse,
|
||||
} from '@kbn/fleet-plugin/common';
|
||||
import { epmRouteService, API_VERSIONS } from '@kbn/fleet-plugin/common';
|
||||
import { epmRouteService, API_VERSIONS, agentPolicyRouteService } from '@kbn/fleet-plugin/common';
|
||||
|
||||
import type { BulkGetAgentPoliciesRequestSchema } from '@kbn/fleet-plugin/server/types';
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import type { NewPolicyData } from '../../../../common/endpoint/types';
|
||||
import type { GetPolicyResponse, UpdatePolicyResponse } from '../../pages/policy/types';
|
||||
|
||||
|
@ -120,3 +123,15 @@ export const sendGetEndpointSecurityPackage = async (
|
|||
}
|
||||
return endpointPackageInfo;
|
||||
};
|
||||
|
||||
export const sendBulkGetAgentPolicies = async ({
|
||||
http,
|
||||
requestBody,
|
||||
}: {
|
||||
http: HttpStart;
|
||||
requestBody: TypeOf<typeof BulkGetAgentPoliciesRequestSchema.body>;
|
||||
}): Promise<BulkGetAgentPoliciesResponse> =>
|
||||
http.post<BulkGetAgentPoliciesResponse>(agentPolicyRouteService.getBulkGetPath(), {
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
|
|
@ -37775,7 +37775,6 @@
|
|||
"xpack.securitySolution.endpoint.list.loadingPolicies": "Chargement des intégrations",
|
||||
"xpack.securitySolution.endpoint.list.noEndpointsInstructions": "Vous avez ajouté l'intégration Elastic Defend. Vous pouvez maintenant enregistrer vos agents en suivant la procédure ci-dessous.",
|
||||
"xpack.securitySolution.endpoint.list.noEndpointsPrompt": "Étape suivante : Enregistrer un agent avec Elastic Defend",
|
||||
"xpack.securitySolution.endpoint.list.noPolicies": "Il n'existe aucune intégration.",
|
||||
"xpack.securitySolution.endpoint.list.os": "Système d'exploitation",
|
||||
"xpack.securitySolution.endpoint.list.pageSubTitle": "Hôtes exécutant Elastic Defend",
|
||||
"xpack.securitySolution.endpoint.list.pageTitle": "Points de terminaison",
|
||||
|
@ -47699,4 +47698,4 @@
|
|||
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "Ce champ est requis.",
|
||||
"xpack.watcher.watcherDescription": "Détectez les modifications survenant dans vos données en créant, gérant et monitorant des alertes."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37517,7 +37517,6 @@
|
|||
"xpack.securitySolution.endpoint.list.loadingPolicies": "統合を読み込んでいます",
|
||||
"xpack.securitySolution.endpoint.list.noEndpointsInstructions": "Elastic Defend統合を追加しました。次の手順を使用して、エージェントを登録してください。",
|
||||
"xpack.securitySolution.endpoint.list.noEndpointsPrompt": "次のステップ:Elastic Defendにエージェントを登録する",
|
||||
"xpack.securitySolution.endpoint.list.noPolicies": "統合はありません。",
|
||||
"xpack.securitySolution.endpoint.list.os": "OS",
|
||||
"xpack.securitySolution.endpoint.list.pageSubTitle": "Elastic Defendを実行しているホスト",
|
||||
"xpack.securitySolution.endpoint.list.pageTitle": "エンドポイント",
|
||||
|
@ -47436,4 +47435,4 @@
|
|||
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
|
||||
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37563,7 +37563,6 @@
|
|||
"xpack.securitySolution.endpoint.list.loadingPolicies": "正在加载集成",
|
||||
"xpack.securitySolution.endpoint.list.noEndpointsInstructions": "您已添加 Elastic Defend 集成。现在,按照以下步骤注册您的代理。",
|
||||
"xpack.securitySolution.endpoint.list.noEndpointsPrompt": "下一步:将代理注册到 Elastic Defend",
|
||||
"xpack.securitySolution.endpoint.list.noPolicies": "没有集成。",
|
||||
"xpack.securitySolution.endpoint.list.os": "OS",
|
||||
"xpack.securitySolution.endpoint.list.pageSubTitle": "运行 Elastic Defend 的主机",
|
||||
"xpack.securitySolution.endpoint.list.pageTitle": "终端",
|
||||
|
@ -47490,4 +47489,4 @@
|
|||
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。",
|
||||
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue