-
Notifications
You must be signed in to change notification settings - Fork 8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Security Solution][Endpoint] Artifacts event filter card on integration policy edit view #121879
Changes from all commits
6a059c8
5d78e67
9858e56
1c25b10
43675ea
3bc3a04
ef5911d
dbd2242
6b5c6ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,8 +46,8 @@ export const StyledEuiFlexGridGroup = styled(EuiFlexGroup)` | |
const StyledEuiFlexGroup = styled(EuiFlexGroup)<{ | ||
isSmall: boolean; | ||
}>` | ||
font-size: ${({ isSmall, theme }) => (isSmall ? theme.eui.euiFontSizeXS : 'innherit')}; | ||
font-weight: ${({ isSmall }) => (isSmall ? '1px' : 'innherit')}; | ||
font-size: ${({ isSmall, theme }) => (isSmall ? theme.eui.euiFontSizeXS : 'inherit')}; | ||
font-weight: ${({ isSmall }) => (isSmall ? '1px' : 'inherit')}; | ||
Comment on lines
48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I missed this in an earlier review! 😅 |
||
`; | ||
|
||
const CSS_BOLD: Readonly<React.CSSProperties> = { fontWeight: 'bold' }; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* | ||
* 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 { waitFor, act } from '@testing-library/react'; | ||
import * as reactTestingLibrary from '@testing-library/react'; | ||
import { | ||
AppContextTestRender, | ||
createAppRootMockRenderer, | ||
} from '../../../../../../../common/mock/endpoint'; | ||
|
||
import { eventFiltersListQueryHttpMock } from '../../../../../event_filters/test_utils'; | ||
import { FleetIntegrationEventFiltersCard } from './fleet_integration_event_filters_card'; | ||
import { EndpointDocGenerator } from '../../../../../../../../common/endpoint/generate_data'; | ||
import { getPolicyEventFiltersPath } from '../../../../../../common/routing'; | ||
import { PolicyData } from '../../../../../../../../common/endpoint/types'; | ||
import { getFoundExceptionListItemSchemaMock } from '../../../../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; | ||
|
||
const endpointGenerator = new EndpointDocGenerator('seed'); | ||
|
||
describe('Fleet integration policy endpoint security event filters card', () => { | ||
let render: () => Promise<ReturnType<AppContextTestRender['render']>>; | ||
let renderResult: ReturnType<AppContextTestRender['render']>; | ||
let history: AppContextTestRender['history']; | ||
let mockedContext: AppContextTestRender; | ||
let mockedApi: ReturnType<typeof eventFiltersListQueryHttpMock>; | ||
let policy: PolicyData; | ||
|
||
beforeEach(() => { | ||
policy = endpointGenerator.generatePolicyPackagePolicy(); | ||
mockedContext = createAppRootMockRenderer(); | ||
mockedApi = eventFiltersListQueryHttpMock(mockedContext.coreStart.http); | ||
({ history } = mockedContext); | ||
render = async () => { | ||
await act(async () => { | ||
renderResult = mockedContext.render( | ||
<FleetIntegrationEventFiltersCard policyId={policy.id} /> | ||
); | ||
await waitFor(() => expect(mockedApi.responseProvider.eventFiltersList).toHaveBeenCalled()); | ||
}); | ||
return renderResult; | ||
}; | ||
|
||
history.push(getPolicyEventFiltersPath(policy.id)); | ||
}); | ||
|
||
afterEach(() => reactTestingLibrary.cleanup()); | ||
|
||
it('should call the API and render the card correctly', async () => { | ||
mockedApi.responseProvider.eventFiltersList.mockReturnValue( | ||
getFoundExceptionListItemSchemaMock(3) | ||
); | ||
|
||
await render(); | ||
expect(renderResult.getByTestId('eventFilters-fleet-integration-card')).toHaveTextContent( | ||
'Event filters3' | ||
); | ||
}); | ||
|
||
it('should show the card even when no event filters associated with the policy', async () => { | ||
mockedApi.responseProvider.eventFiltersList.mockReturnValue( | ||
getFoundExceptionListItemSchemaMock(0) | ||
); | ||
|
||
await render(); | ||
expect(renderResult.getByTestId('eventFilters-fleet-integration-card')).toBeTruthy(); | ||
}); | ||
|
||
it('should have the correct manage event filters link', async () => { | ||
mockedApi.responseProvider.eventFiltersList.mockReturnValue( | ||
getFoundExceptionListItemSchemaMock(1) | ||
); | ||
|
||
await render(); | ||
expect(renderResult.getByTestId('eventFilters-link-to-exceptions')).toHaveAttribute( | ||
'href', | ||
`/app/security/administration/policy/${policy.id}/eventFilters` | ||
); | ||
}); | ||
|
||
it('should show an error toast when API request fails', async () => { | ||
const error = new Error('Uh oh! API error!'); | ||
mockedApi.responseProvider.eventFiltersList.mockImplementation(() => { | ||
throw error; | ||
}); | ||
|
||
await render(); | ||
await waitFor(() => { | ||
expect(mockedContext.coreStart.notifications.toasts.addDanger).toHaveBeenCalledTimes(1); | ||
expect(mockedContext.coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( | ||
`There was an error trying to fetch event filters stats: "${error}"` | ||
); | ||
}); | ||
}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a test case for the error handling? A test that checks a toast is shown when there is an API error. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done 3bc3a04 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
/* | ||
* 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 { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { FormattedMessage } from '@kbn/i18n-react'; | ||
import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; | ||
import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common'; | ||
import { pagePathGetters } from '../../../../../../../../../fleet/public'; | ||
import { | ||
GetExceptionSummaryResponse, | ||
PolicyDetailsRouteState, | ||
} from '../../../../../../../../common/endpoint/types'; | ||
import { useAppUrl, useHttp, useToasts } from '../../../../../../../common/lib/kibana'; | ||
import { getPolicyEventFiltersPath } from '../../../../../../common/routing'; | ||
import { parsePoliciesToKQL } from '../../../../../../common/utils'; | ||
import { ExceptionItemsSummary } from './exception_items_summary'; | ||
import { LinkWithIcon } from './link_with_icon'; | ||
import { StyledEuiFlexItem } from './styled_components'; | ||
import { EventFiltersHttpService } from '../../../../../event_filters/service'; | ||
|
||
export const FleetIntegrationEventFiltersCard = memo<{ | ||
policyId: string; | ||
}>(({ policyId }) => { | ||
const toasts = useToasts(); | ||
const http = useHttp(); | ||
const [stats, setStats] = useState<GetExceptionSummaryResponse | undefined>(); | ||
const isMounted = useRef<boolean>(); | ||
const { getAppUrl } = useAppUrl(); | ||
|
||
const eventFiltersApi = useMemo(() => new EventFiltersHttpService(http), [http]); | ||
const policyEventFiltersPath = getPolicyEventFiltersPath(policyId); | ||
|
||
const policyEventFiltersRouteState = useMemo<PolicyDetailsRouteState>(() => { | ||
const fleetPackageIntegrationCustomUrlPath = `#${ | ||
pagePathGetters.integration_policy_edit({ packagePolicyId: policyId })[1] | ||
}`; | ||
|
||
return { | ||
backLink: { | ||
label: i18n.translate( | ||
'xpack.securitySolution.endpoint.fleetCustomExtension.artifacts.backButtonLabel', | ||
{ | ||
defaultMessage: `Back to Fleet integration policy`, | ||
} | ||
), | ||
navigateTo: [ | ||
INTEGRATIONS_PLUGIN_ID, | ||
{ | ||
path: fleetPackageIntegrationCustomUrlPath, | ||
}, | ||
], | ||
href: getAppUrl({ | ||
appId: INTEGRATIONS_PLUGIN_ID, | ||
path: fleetPackageIntegrationCustomUrlPath, | ||
}), | ||
}, | ||
}; | ||
}, [getAppUrl, policyId]); | ||
|
||
const linkToEventFilters = useMemo( | ||
() => ( | ||
<LinkWithIcon | ||
href={getAppUrl({ | ||
path: policyEventFiltersPath, | ||
})} | ||
appPath={policyEventFiltersPath} | ||
appState={policyEventFiltersRouteState} | ||
data-test-subj="eventFilters-link-to-exceptions" | ||
size="m" | ||
> | ||
<FormattedMessage | ||
id="xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersManageLabel" | ||
defaultMessage="Manage event filters" | ||
/> | ||
</LinkWithIcon> | ||
), | ||
[getAppUrl, policyEventFiltersPath, policyEventFiltersRouteState] | ||
); | ||
|
||
useEffect(() => { | ||
isMounted.current = true; | ||
const fetchStats = async () => { | ||
try { | ||
const summary = await eventFiltersApi.getList({ | ||
perPage: 1, | ||
page: 1, | ||
filter: parsePoliciesToKQL([policyId, 'all']), | ||
}); | ||
if (isMounted.current) { | ||
setStats({ | ||
total: summary.total, | ||
windows: 0, | ||
linux: 0, | ||
macos: 0, | ||
}); | ||
} | ||
} catch (error) { | ||
if (isMounted.current) { | ||
toasts.addDanger( | ||
i18n.translate( | ||
'xpack.securitySolution.endpoint.fleetCustomExtension.eventFiltersSummary.error', | ||
{ | ||
defaultMessage: 'There was an error trying to fetch event filters stats: "{error}"', | ||
values: { error }, | ||
} | ||
) | ||
); | ||
} | ||
} | ||
}; | ||
fetchStats(); | ||
return () => { | ||
isMounted.current = false; | ||
}; | ||
}, [eventFiltersApi, policyId, toasts]); | ||
|
||
return ( | ||
<EuiPanel | ||
hasShadow={false} | ||
paddingSize="l" | ||
hasBorder | ||
data-test-subj="eventFilters-fleet-integration-card" | ||
> | ||
<EuiFlexGroup | ||
alignItems="baseline" | ||
justifyContent="flexStart" | ||
gutterSize="s" | ||
direction="row" | ||
responsive={false} | ||
> | ||
<EuiFlexItem grow={false}> | ||
<EuiText> | ||
<h5> | ||
<FormattedMessage | ||
id="xpack.securitySolution.endpoint.eventFilters.fleetIntegration.title" | ||
defaultMessage="Event filters" | ||
/> | ||
</h5> | ||
</EuiText> | ||
</EuiFlexItem> | ||
<EuiFlexItem grow={false}> | ||
<ExceptionItemsSummary stats={stats} isSmall={true} /> | ||
</EuiFlexItem> | ||
<StyledEuiFlexItem grow={1}>{linkToEventFilters}</StyledEuiFlexItem> | ||
</EuiFlexGroup> | ||
</EuiPanel> | ||
); | ||
}); | ||
|
||
FleetIntegrationEventFiltersCard.displayName = 'FleetIntegrationEventFiltersCard'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉