mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[AI4DSOC] Add configurations integrations page (#217905)
## Summary Implements the curated Integrations management page for AI for the SOC: - Adds the new Integrations page utilizing the `PackageListGrid` exported fleet component - Paths of `/configurations/integrations/browse` and `configurations/integrations/installed` to be consistent with current fleet pages `/integrations/browse` and `/integrations/installed` - Updates the `PackageCard` to expose new settings options defaulted to the existing behavior - Updates the sidebar link order to match the tabs <img width="1722" alt="Screenshot 2025-04-14 at 12 00 41 PM" src="https://github.com/user-attachments/assets/982e01b9-4ceb-4a1e-9cfe-4a44d2f9c8bf" /> <img width="1720" alt="Screenshot 2025-04-14 at 12 00 55 PM" src="https://github.com/user-attachments/assets/401f37fe-791f-4f7c-b31f-f0d6b56f1b46" /> <img width="517" alt="Screenshot 2025-04-10 at 3 11 29 PM" src="https://github.com/user-attachments/assets/f60e6eda-6750-40fb-8611-e73ef5d8fa91" /> ## How to test - Add the following to `serverless.security.dev.yml`: ``` xpack.securitySolutionServerless.productTypes: [ { product_line: 'ai_soc', product_tier: 'search_ai_lake' }, ] ``` - Run Kibana serverless for security - Verify behavior matches the UX mockups: [figma](https://www.figma.com/design/DYs7j4GQdAhg7aWTLI4R69/AI4DSOC?node-id=2969-143558&p=f&m=dev) Couple things to note: - some of the actual logos vary slightly from the figma, but UX has approved - if you want to actually install integrations in agentless, be sure to reference fleet docs for [serverless ](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/developing_kibana_in_serverless.md) and [agentless](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/fleet/dev_docs/local_setup/agentless.md). If you just want to get an idea of what it would look like installed without doing that, just navigate to the Settings tab on the integrations overview and install its assets Relates: https://github.com/elastic/security-team/issues/11789
This commit is contained in:
parent
31f0f211d9
commit
a63876b1a4
21 changed files with 745 additions and 26 deletions
|
@ -68,11 +68,13 @@ export function PackageCard({
|
|||
titleLineClamp,
|
||||
descriptionLineClamp,
|
||||
maxCardHeight,
|
||||
showDescription = true,
|
||||
showReleaseBadge = true,
|
||||
}: PackageCardProps) {
|
||||
const theme = useEuiTheme();
|
||||
|
||||
let releaseBadge: React.ReactNode | null = null;
|
||||
if (release && release !== 'ga') {
|
||||
if (release && release !== 'ga' && showReleaseBadge) {
|
||||
releaseBadge = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="xs" />
|
||||
|
@ -220,7 +222,7 @@ export function PackageCard({
|
|||
${getLineClampStyles(titleLineClamp)}
|
||||
}
|
||||
|
||||
min-height: 127px;
|
||||
min-height: ${showDescription ? '127px' : null};
|
||||
border-color: ${isQuickstart ? theme.euiTheme.colors.accent : null};
|
||||
max-height: ${maxCardHeight ? `${maxCardHeight}px` : null};
|
||||
overflow: ${maxCardHeight ? 'hidden' : null};
|
||||
|
@ -230,7 +232,7 @@ export function PackageCard({
|
|||
layout="horizontal"
|
||||
title={title || ''}
|
||||
titleSize="xs"
|
||||
description={description}
|
||||
description={showDescription ? description : ''}
|
||||
hasBorder
|
||||
icon={
|
||||
<CardIcon
|
||||
|
@ -238,7 +240,7 @@ export function PackageCard({
|
|||
packageName={name}
|
||||
integrationName={integration}
|
||||
version={version}
|
||||
size="xl"
|
||||
size={showDescription ? 'xl' : 'xxl'}
|
||||
/>
|
||||
}
|
||||
onClick={onClickProp ?? onCardClick}
|
||||
|
|
|
@ -58,8 +58,10 @@ export interface IntegrationCardItem {
|
|||
name: string;
|
||||
onCardClick?: () => void;
|
||||
release?: IntegrationCardReleaseLabel;
|
||||
showDescription?: boolean;
|
||||
showInstallationStatus?: boolean;
|
||||
showLabels?: boolean;
|
||||
showReleaseBadge?: boolean;
|
||||
title: string;
|
||||
// Security Solution uses this prop to determine how many lines the card title should be truncated
|
||||
titleLineClamp?: number;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { useEnhancedIntegrationCards } from './integrations/use_enhanced_integration_cards';
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 const RETURN_APP_ID = 'returnAppId';
|
||||
export const RETURN_PATH = 'returnPath';
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* 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 {
|
||||
applyCategoryBadgeAndStyling,
|
||||
useEnhancedIntegrationCards,
|
||||
getCategoryBadgeIfAny,
|
||||
} from './use_enhanced_integration_cards';
|
||||
import { IntegrationsFacets } from '../../../../../configurations/constants';
|
||||
import type { IntegrationCardItem } from '@kbn/fleet-plugin/public';
|
||||
import { installationStatuses } from '@kbn/fleet-plugin/public';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
const mockCard = (name: string, categories?: string[]) =>
|
||||
({
|
||||
id: `epr:${name}`,
|
||||
description: 'description',
|
||||
icons: [],
|
||||
title: name,
|
||||
url: `/app/integrations/detail/${name}-1.0.0/overview`,
|
||||
integration: '',
|
||||
name,
|
||||
version: '1.0.0',
|
||||
release: 'ga',
|
||||
categories: categories ?? [],
|
||||
isUnverified: false,
|
||||
} as IntegrationCardItem);
|
||||
|
||||
describe('applyCategoryBadgeAndStyling', () => {
|
||||
const mockInt = mockCard('crowdstrike', ['edr_xdr']);
|
||||
|
||||
it('should add the correct return path to the URL', () => {
|
||||
const callerView = IntegrationsFacets.available;
|
||||
const result = applyCategoryBadgeAndStyling(mockInt, callerView);
|
||||
|
||||
const urlParams = new URLSearchParams(result.url.split('?')[1]);
|
||||
expect(urlParams.get('returnPath')).toBe(`/configurations/integrations/${callerView}`);
|
||||
});
|
||||
|
||||
it('should add the EDR/XDR badge if the category includes edr_xdr', () => {
|
||||
const cardWithEdrXdr = { ...mockInt, categories: ['edr_xdr'] };
|
||||
const result = applyCategoryBadgeAndStyling(cardWithEdrXdr, IntegrationsFacets.available);
|
||||
|
||||
expect(result.extraLabelsBadges).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should add the SIEM badge if the category includes siem', () => {
|
||||
const cardWithSiem = { ...mockInt, categories: ['siem'] };
|
||||
const result = applyCategoryBadgeAndStyling(cardWithSiem, IntegrationsFacets.available);
|
||||
|
||||
expect(result.extraLabelsBadges).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not add any badge if the category does not include edr_xdr or siem', () => {
|
||||
const cardWithOtherCategory = { ...mockInt, categories: ['other'] };
|
||||
const result = applyCategoryBadgeAndStyling(
|
||||
cardWithOtherCategory,
|
||||
IntegrationsFacets.available
|
||||
);
|
||||
|
||||
expect(result.extraLabelsBadges).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should set showDescription and showReleaseBadge to false', () => {
|
||||
const result = applyCategoryBadgeAndStyling(mockInt, IntegrationsFacets.available);
|
||||
|
||||
expect(result.showDescription).toBe(false);
|
||||
expect(result.showReleaseBadge).toBe(false);
|
||||
});
|
||||
|
||||
it('should set maxCardHeight to 88', () => {
|
||||
const result = applyCategoryBadgeAndStyling(mockInt, IntegrationsFacets.available);
|
||||
|
||||
expect(result.maxCardHeight).toBe(88);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useEnhancedIntegrationCards', () => {
|
||||
const intA = mockCard('crowdstrike', ['edr_xdr']);
|
||||
const intB = mockCard('google_secops', ['siem']);
|
||||
const intC = mockCard('microsoft_sentinel', ['siem']);
|
||||
const intD = mockCard('sentinel_one', ['edr_xdr']);
|
||||
|
||||
it('should return sorted available integrations with badges applied', () => {
|
||||
const mockIntegrationsList = [intA, intB, intC, intD];
|
||||
const { result } = renderHook(() => useEnhancedIntegrationCards(mockIntegrationsList));
|
||||
|
||||
expect(result.current.available).toHaveLength(4);
|
||||
expect(result.current.available[0].id).toBe('epr:google_secops');
|
||||
expect(result.current.available[1].id).toBe('epr:microsoft_sentinel');
|
||||
expect(result.current.available[0].extraLabelsBadges).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should return sorted installed integrations with badges applied', () => {
|
||||
const mockIntegrationsList = [
|
||||
intA,
|
||||
intB,
|
||||
{ ...intC, installStatus: installationStatuses.Installed },
|
||||
intD,
|
||||
];
|
||||
const { result } = renderHook(() => useEnhancedIntegrationCards(mockIntegrationsList));
|
||||
|
||||
expect(result.current.installed).toHaveLength(1);
|
||||
expect(result.current.installed[0].id).toBe('epr:microsoft_sentinel');
|
||||
expect(result.current.installed[0].extraLabelsBadges).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle an empty integrations list', () => {
|
||||
const { result } = renderHook(() => useEnhancedIntegrationCards([]));
|
||||
|
||||
expect(result.current.available).toHaveLength(0);
|
||||
expect(result.current.installed).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should correctly apply custom display order', () => {
|
||||
const mockIntegrationsList = [intA, intB, intC, intD];
|
||||
|
||||
const shuffledList = [
|
||||
mockIntegrationsList[3],
|
||||
mockIntegrationsList[1],
|
||||
mockIntegrationsList[0],
|
||||
mockIntegrationsList[2],
|
||||
];
|
||||
|
||||
const { result } = renderHook(() => useEnhancedIntegrationCards(shuffledList));
|
||||
|
||||
expect(result.current.available[0].id).toBe('epr:google_secops');
|
||||
expect(result.current.available[1].id).toBe('epr:microsoft_sentinel');
|
||||
expect(result.current.available[2].id).toBe('epr:sentinel_one');
|
||||
expect(result.current.available[3].id).toBe('epr:crowdstrike');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCategoryBadgeIfAny', () => {
|
||||
it('should return "EDR/XDR" when the categories include "edr_xdr"', () => {
|
||||
const categories = ['edr_xdr', 'other_category'];
|
||||
const result = getCategoryBadgeIfAny(categories);
|
||||
expect(result).toBe('EDR/XDR');
|
||||
});
|
||||
|
||||
it('should return "SIEM" when the categories include "siem"', () => {
|
||||
const categories = ['siem', 'other_category'];
|
||||
const result = getCategoryBadgeIfAny(categories);
|
||||
expect(result).toBe('SIEM');
|
||||
});
|
||||
|
||||
it('should return "EDR/XDR" when both "edr_xdr" and "siem" are present', () => {
|
||||
const categories = ['edr_xdr', 'siem'];
|
||||
const result = getCategoryBadgeIfAny(categories);
|
||||
// "edr_xdr" takes precedence, but we don't realistically expect both to be present
|
||||
expect(result).toBe('EDR/XDR');
|
||||
});
|
||||
|
||||
it('should return null when neither "edr_xdr" nor "siem" are present', () => {
|
||||
const categories = ['other_category'];
|
||||
const result = getCategoryBadgeIfAny(categories);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when the categories array is empty', () => {
|
||||
const categories: string[] = [];
|
||||
const result = getCategoryBadgeIfAny(categories);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { EuiFlexItem, EuiSpacer, EuiBadge } from '@elastic/eui';
|
||||
import { installationStatuses, type IntegrationCardItem } from '@kbn/fleet-plugin/public';
|
||||
import { SECURITY_UI_APP_ID } from '@kbn/security-solution-navigation';
|
||||
import { CONFIGURATIONS_PATH } from '../../../../../../common/constants';
|
||||
import { IntegrationsFacets } from '../../../../../configurations/constants';
|
||||
import { RETURN_APP_ID, RETURN_PATH } from './constants';
|
||||
|
||||
const FEATURED_INTEGRATION_SORT_ORDER = [
|
||||
'epr:splunk',
|
||||
'epr:google_secops',
|
||||
'epr:microsoft_sentinel',
|
||||
'epr:sentinel_one',
|
||||
'epr:crowdstrike',
|
||||
];
|
||||
const INTEGRATION_CARD_MAX_HEIGHT_PX = 88;
|
||||
|
||||
const addPathParamToUrl = (url: string, path: string) => {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
const paramsString = `${RETURN_APP_ID}=${SECURITY_UI_APP_ID}&${RETURN_PATH}=${encodedPath}`;
|
||||
|
||||
if (url.indexOf('?') >= 0) {
|
||||
return `${url}&${paramsString}`;
|
||||
}
|
||||
return `${url}?${paramsString}`;
|
||||
};
|
||||
|
||||
export const getCategoryBadgeIfAny = (categories: string[]): string | null => {
|
||||
return categories.includes('edr_xdr') ? 'EDR/XDR' : categories.includes('siem') ? 'SIEM' : null;
|
||||
};
|
||||
|
||||
export const applyCategoryBadgeAndStyling = (
|
||||
card: IntegrationCardItem,
|
||||
callerView: IntegrationsFacets
|
||||
): IntegrationCardItem => {
|
||||
const returnPath = `${CONFIGURATIONS_PATH}/integrations/${callerView}`;
|
||||
const url = addPathParamToUrl(card.url, returnPath);
|
||||
const categoryBadge = getCategoryBadgeIfAny(card.categories);
|
||||
return {
|
||||
...card,
|
||||
url,
|
||||
showDescription: false,
|
||||
showReleaseBadge: false,
|
||||
extraLabelsBadges: categoryBadge
|
||||
? ([
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="xs" />
|
||||
<span>
|
||||
<EuiBadge color="hollow">{categoryBadge}</EuiBadge>
|
||||
</span>
|
||||
</EuiFlexItem>,
|
||||
] as React.ReactNode[])
|
||||
: [],
|
||||
maxCardHeight: INTEGRATION_CARD_MAX_HEIGHT_PX,
|
||||
};
|
||||
};
|
||||
|
||||
const applyCustomDisplayOrder = (integrationsList: IntegrationCardItem[]) => {
|
||||
return integrationsList.sort(
|
||||
(a, b) =>
|
||||
FEATURED_INTEGRATION_SORT_ORDER.indexOf(a.id) - FEATURED_INTEGRATION_SORT_ORDER.indexOf(b.id)
|
||||
);
|
||||
};
|
||||
|
||||
export const useEnhancedIntegrationCards = (
|
||||
integrationsList: IntegrationCardItem[]
|
||||
): { available: IntegrationCardItem[]; installed: IntegrationCardItem[] } => {
|
||||
const sorted = applyCustomDisplayOrder(integrationsList);
|
||||
|
||||
const available = useMemo(
|
||||
() => sorted.map((card) => applyCategoryBadgeAndStyling(card, IntegrationsFacets.available)),
|
||||
[sorted]
|
||||
);
|
||||
|
||||
const installed = useMemo(
|
||||
() =>
|
||||
sorted
|
||||
.map((card) => applyCategoryBadgeAndStyling(card, IntegrationsFacets.installed))
|
||||
.filter(
|
||||
(card) =>
|
||||
card.installStatus === installationStatuses.Installed ||
|
||||
card.installStatus === installationStatuses.InstallFailed
|
||||
),
|
||||
[sorted]
|
||||
);
|
||||
|
||||
return { available, installed };
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/** Allow list of integrations to be available in the AI4DSOC integrations page */
|
||||
export const SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS: string[] = [
|
||||
'crowdstrike',
|
||||
'google_secops',
|
||||
'microsoft_sentinel',
|
||||
'sentinel_one',
|
||||
'splunk',
|
||||
];
|
|
@ -10,3 +10,8 @@ export enum ConfigurationTabs {
|
|||
basicRules = 'basic_rules',
|
||||
aiSettings = 'ai_settings',
|
||||
}
|
||||
|
||||
export enum IntegrationsFacets {
|
||||
available = 'browse',
|
||||
installed = 'installed',
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../../../..',
|
||||
roots: ['<rootDir>/x-pack/solutions/security/plugins/security_solution/public/configurations'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/solutions/security/plugins/security_solution/public/configurations',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/solutions/security/plugins/security_solution/public/configurations/**/*.{ts,tsx}',
|
||||
],
|
||||
moduleNameMapper: require('../../server/__mocks__/module_name_map'),
|
||||
};
|
|
@ -11,16 +11,16 @@ import { Routes, Route } from '@kbn/shared-ux-router';
|
|||
import { Redirect } from 'react-router-dom';
|
||||
import { BasicRules } from '../tabs/basic_rules';
|
||||
import { AiSettings } from '../tabs/ai_settings';
|
||||
import { Integrations } from '../tabs/integrations';
|
||||
import { CONFIGURATIONS_PATH } from '../../../common/constants';
|
||||
import { ConfigurationTabs } from '../constants';
|
||||
import { LazyConfigurationsIntegrationsHome } from '../tabs/integrations';
|
||||
|
||||
export const ConfigurationsRouter = React.memo(() => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path={`${CONFIGURATIONS_PATH}/:tab(${ConfigurationTabs.integrations})`}
|
||||
component={Integrations}
|
||||
component={LazyConfigurationsIntegrationsHome}
|
||||
/>
|
||||
<Route
|
||||
path={`${CONFIGURATIONS_PATH}/:tab(${ConfigurationTabs.aiSettings})`}
|
||||
|
|
|
@ -4,8 +4,82 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
|
||||
export const Integrations = () => {
|
||||
return <h1>{'Integrations'}</h1>;
|
||||
};
|
||||
import React from 'react';
|
||||
import { EuiSkeletonLoading } from '@elastic/eui';
|
||||
import type { AvailablePackagesHookType } from '@kbn/fleet-plugin/public';
|
||||
import { Routes, Route } from '@kbn/shared-ux-router';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { CONFIGURATIONS_PATH } from '../../../common/constants';
|
||||
import { SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS } from '../../common/lib/search_ai_lake/integrations';
|
||||
import { useEnhancedIntegrationCards } from '../../common/lib/search_ai_lake/hooks';
|
||||
import { ConfigurationTabs, IntegrationsFacets } from '../constants';
|
||||
import { IntegrationsPage, IntegrationsSkeleton } from './integrations/components';
|
||||
import { withLazyHook } from '../../common/components/with_lazy_hook';
|
||||
|
||||
export interface IntegrationsPageProps {
|
||||
useAvailablePackages: AvailablePackagesHookType;
|
||||
}
|
||||
|
||||
export const ConfigurationsIntegrationsHome = React.memo<IntegrationsPageProps>(
|
||||
({ useAvailablePackages }) => {
|
||||
const { filteredCards, isLoading, searchTerm, setSearchTerm } = useAvailablePackages({
|
||||
prereleaseIntegrationsEnabled: true,
|
||||
});
|
||||
|
||||
const allowedIntegrations = filteredCards.filter((card) =>
|
||||
SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS.includes(card.name)
|
||||
);
|
||||
|
||||
const { available, installed } = useEnhancedIntegrationCards(allowedIntegrations);
|
||||
|
||||
return (
|
||||
<EuiSkeletonLoading
|
||||
isLoading={isLoading}
|
||||
loadingContent={<IntegrationsSkeleton />}
|
||||
loadedContent={
|
||||
<>
|
||||
<Routes>
|
||||
<Route
|
||||
path={`${CONFIGURATIONS_PATH}/${ConfigurationTabs.integrations}/:view(${IntegrationsFacets.available})`}
|
||||
>
|
||||
<IntegrationsPage
|
||||
view={IntegrationsFacets.available}
|
||||
availableIntegrations={available}
|
||||
installedIntegrations={installed}
|
||||
searchTerm={searchTerm}
|
||||
setSearchTerm={setSearchTerm}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={`${CONFIGURATIONS_PATH}/${ConfigurationTabs.integrations}/:view(${IntegrationsFacets.installed})`}
|
||||
>
|
||||
<IntegrationsPage
|
||||
view={IntegrationsFacets.installed}
|
||||
availableIntegrations={available}
|
||||
installedIntegrations={installed}
|
||||
searchTerm={searchTerm}
|
||||
setSearchTerm={setSearchTerm}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path={`${CONFIGURATIONS_PATH}/${ConfigurationTabs.integrations}`}
|
||||
render={() => (
|
||||
<Redirect
|
||||
to={`${CONFIGURATIONS_PATH}/${ConfigurationTabs.integrations}/${IntegrationsFacets.available}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Routes>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
ConfigurationsIntegrationsHome.displayName = 'ConfigurationsIntegrationsHome';
|
||||
|
||||
export const LazyConfigurationsIntegrationsHome = withLazyHook(ConfigurationsIntegrationsHome, () =>
|
||||
import('@kbn/fleet-plugin/public').then((module) => module.AvailablePackagesHook())
|
||||
);
|
||||
LazyConfigurationsIntegrationsHome.displayName = 'LazyConfigurationsIntegrationsHome';
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 const FACETS_MAX_WIDTH_PX = 216;
|
||||
export const INTEGRATIONS_GRID_MAX_WIDTH_PX = 1200;
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { IntegrationsPage } from './integrations_page';
|
||||
export { IntegrationsSkeleton } from './integrations_skeleton';
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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, { lazy } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import type { IntegrationCardItem } from '@kbn/fleet-plugin/public';
|
||||
import { noop } from 'lodash';
|
||||
import { FACETS_MAX_WIDTH_PX, INTEGRATIONS_GRID_MAX_WIDTH_PX } from './constants';
|
||||
import { IntegrationViewFacets } from './view_facets';
|
||||
import { IntegrationsFacets } from '../../../constants';
|
||||
|
||||
export const PackageListGrid = lazy(async () => ({
|
||||
default: await import('@kbn/fleet-plugin/public')
|
||||
.then((module) => module.PackageList())
|
||||
.then((pkg) => pkg.PackageListGrid),
|
||||
}));
|
||||
|
||||
export interface IntegrationsGridProps {
|
||||
view: IntegrationsFacets;
|
||||
availableIntegrations: IntegrationCardItem[];
|
||||
installedIntegrations: IntegrationCardItem[];
|
||||
searchTerm: string;
|
||||
setSearchTerm: (searchTerm: string) => void;
|
||||
}
|
||||
|
||||
export const IntegrationsPage = React.memo<IntegrationsGridProps>(
|
||||
({ view, availableIntegrations, installedIntegrations, searchTerm, setSearchTerm }) => {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
max-width: ${FACETS_MAX_WIDTH_PX}px;
|
||||
`}
|
||||
>
|
||||
<IntegrationViewFacets
|
||||
allCount={availableIntegrations.length}
|
||||
installedCount={installedIntegrations.length}
|
||||
selectedFacet={view}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
max-width: ${INTEGRATIONS_GRID_MAX_WIDTH_PX}px;
|
||||
`}
|
||||
>
|
||||
<PackageListGrid
|
||||
calloutTopSpacerSize="m"
|
||||
categories={[]} // We do not want to show categories and subcategories as the search bar filter
|
||||
emptyStateStyles={{ paddingTop: '16px' }}
|
||||
list={
|
||||
view === IntegrationsFacets.available
|
||||
? availableIntegrations
|
||||
: view === IntegrationsFacets.installed
|
||||
? installedIntegrations
|
||||
: []
|
||||
}
|
||||
scrollElementId={'integrations-scroll-container'}
|
||||
selectedCategory={'security'}
|
||||
selectedSubCategory={''}
|
||||
searchTerm={searchTerm}
|
||||
setSearchTerm={setSearchTerm}
|
||||
setCategory={noop}
|
||||
setUrlandPushHistory={noop}
|
||||
setUrlandReplaceHistory={noop}
|
||||
showCardLabels={true}
|
||||
showControls={false}
|
||||
showSearchTools={true}
|
||||
sortByFeaturedIntegrations={false}
|
||||
spacer={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
||||
IntegrationsPage.displayName = 'IntegrationsGrid';
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
import { EuiFlexGroup, EuiSkeletonRectangle, EuiSpacer } from '@elastic/eui';
|
||||
import { FACETS_MAX_WIDTH_PX, INTEGRATIONS_GRID_MAX_WIDTH_PX } from './constants';
|
||||
|
||||
const FACET_LOADING_WIDTH = '216px';
|
||||
const FACET_LOADING_HEIGHT = '20px';
|
||||
|
||||
const SEARCH_BAR_LOADING_WIDTH = '872px';
|
||||
const SEARCH_BAR_LOADING_HEIGHT = '40px';
|
||||
|
||||
const CARD_LOADING_WIDTH = '279px';
|
||||
const CARD_LOADING_HEIGHT = '88px';
|
||||
|
||||
export const IntegrationsSkeleton: React.FC = () => (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
max-width: ${FACETS_MAX_WIDTH_PX}px;
|
||||
`}
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiSkeletonRectangle width={FACET_LOADING_WIDTH} height={FACET_LOADING_HEIGHT} />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSkeletonRectangle width={FACET_LOADING_WIDTH} height={FACET_LOADING_HEIGHT} />
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
max-width: ${INTEGRATIONS_GRID_MAX_WIDTH_PX}px;
|
||||
`}
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiSkeletonRectangle width={SEARCH_BAR_LOADING_WIDTH} height={SEARCH_BAR_LOADING_HEIGHT} />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiSkeletonRectangle width={CARD_LOADING_WIDTH} height={CARD_LOADING_HEIGHT} />
|
||||
<EuiSkeletonRectangle width={CARD_LOADING_WIDTH} height={CARD_LOADING_HEIGHT} />
|
||||
<EuiSkeletonRectangle width={CARD_LOADING_WIDTH} height={CARD_LOADING_HEIGHT} />
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiSkeletonRectangle width={CARD_LOADING_WIDTH} height={CARD_LOADING_HEIGHT} />
|
||||
<EuiSkeletonRectangle width={CARD_LOADING_WIDTH} height={CARD_LOADING_HEIGHT} />
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { IntegrationsFacets } from '../../../constants';
|
||||
import { IntegrationViewFacets, ALL, INSTALLED } from './view_facets';
|
||||
import { useNavigation } from '../../../../common/lib/kibana';
|
||||
import { SecurityPageName } from '@kbn/deeplinks-security';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana', () => ({
|
||||
useNavigation: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('IntegrationViewFacets', () => {
|
||||
const mockNavigateTo = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
(useNavigation as jest.Mock).mockReturnValue({
|
||||
navigateTo: mockNavigateTo,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
allCount: 10,
|
||||
installedCount: 5,
|
||||
selectedFacet: IntegrationsFacets.available,
|
||||
};
|
||||
|
||||
it('renders the "All integrations" and "Installed integrations" buttons', () => {
|
||||
render(<IntegrationViewFacets {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(ALL)).toBeInTheDocument();
|
||||
expect(screen.getByText(INSTALLED)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls navigateTo with the correct path when "All integrations" is clicked', () => {
|
||||
render(<IntegrationViewFacets {...defaultProps} />);
|
||||
|
||||
const allButton = screen.getByTestId('configurations.integrationsAll');
|
||||
fireEvent.click(allButton);
|
||||
|
||||
expect(mockNavigateTo).toHaveBeenCalledWith({
|
||||
deepLinkId: SecurityPageName.configurationsIntegrations,
|
||||
path: 'browse',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls navigateTo with the correct path when "Installed integrations" is clicked', () => {
|
||||
render(
|
||||
<IntegrationViewFacets {...defaultProps} selectedFacet={IntegrationsFacets.installed} />
|
||||
);
|
||||
|
||||
const installedButton = screen.getByTestId('configurations.integrationsInstalled');
|
||||
fireEvent.click(installedButton);
|
||||
|
||||
expect(mockNavigateTo).toHaveBeenCalledWith({
|
||||
deepLinkId: SecurityPageName.configurationsIntegrations,
|
||||
path: 'installed',
|
||||
});
|
||||
});
|
||||
|
||||
it('highlights the correct button based on the selectedFacet prop', () => {
|
||||
const { rerender } = render(<IntegrationViewFacets {...defaultProps} />);
|
||||
|
||||
const allButton = screen.getByText('All integrations');
|
||||
const installedButton = screen.getByText('Installed integrations');
|
||||
|
||||
const selectedFacetClass = 'euiFacetButton__text css-kw0vmr-euiFacetButton__text-isSelected';
|
||||
|
||||
// Check if the "All integrations" button is selected
|
||||
expect(allButton).toHaveClass(selectedFacetClass);
|
||||
expect(installedButton).not.toHaveClass(selectedFacetClass);
|
||||
|
||||
// Rerender with the "Installed integrations" selected
|
||||
rerender(
|
||||
<IntegrationViewFacets {...defaultProps} selectedFacet={IntegrationsFacets.installed} />
|
||||
);
|
||||
|
||||
// Check if the "Installed integrations" button is selected
|
||||
expect(allButton).not.toHaveClass(selectedFacetClass);
|
||||
expect(installedButton).toHaveClass(selectedFacetClass);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { EuiFacetGroup, EuiFacetButton } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SecurityPageName } from '@kbn/deeplinks-security';
|
||||
import { IntegrationsFacets } from '../../../constants';
|
||||
import { useNavigation } from '../../../../common/lib/kibana';
|
||||
|
||||
export interface Props {
|
||||
allCount: number;
|
||||
installedCount: number;
|
||||
selectedFacet: IntegrationsFacets;
|
||||
}
|
||||
|
||||
export const ALL = i18n.translate('xpack.securitySolution.configurations.integrations.allFacet', {
|
||||
defaultMessage: 'All integrations',
|
||||
});
|
||||
|
||||
export const INSTALLED = i18n.translate(
|
||||
'xpack.securitySolution.configurations.integrations.installedFacet',
|
||||
{
|
||||
defaultMessage: 'Installed integrations',
|
||||
}
|
||||
);
|
||||
|
||||
export function IntegrationViewFacets({ allCount, installedCount, selectedFacet }: Props) {
|
||||
const { navigateTo } = useNavigation();
|
||||
|
||||
return (
|
||||
<EuiFacetGroup>
|
||||
<EuiFacetButton
|
||||
css={css`
|
||||
padding-inline: 0px;
|
||||
`}
|
||||
id="integrationsAll"
|
||||
quantity={allCount}
|
||||
isSelected={selectedFacet === IntegrationsFacets.available}
|
||||
data-test-subj={'configurations.integrationsAll'}
|
||||
onClick={() =>
|
||||
navigateTo({
|
||||
deepLinkId: SecurityPageName.configurationsIntegrations,
|
||||
path: 'browse',
|
||||
})
|
||||
}
|
||||
>
|
||||
{ALL}
|
||||
</EuiFacetButton>
|
||||
<EuiFacetButton
|
||||
css={css`
|
||||
padding-inline: 0px;
|
||||
`}
|
||||
id="integrationsInstalled"
|
||||
quantity={installedCount}
|
||||
isSelected={selectedFacet === IntegrationsFacets.installed}
|
||||
data-test-subj={'configurations.integrationsInstalled'}
|
||||
onClick={() =>
|
||||
navigateTo({
|
||||
deepLinkId: SecurityPageName.configurationsIntegrations,
|
||||
path: 'installed',
|
||||
})
|
||||
}
|
||||
>
|
||||
{INSTALLED}
|
||||
</EuiFacetButton>
|
||||
</EuiFacetGroup>
|
||||
);
|
||||
}
|
|
@ -8,15 +8,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import type { PackageListItem } from '@kbn/fleet-plugin/common';
|
||||
import { installationStatuses, useGetPackagesQuery } from '@kbn/fleet-plugin/public';
|
||||
|
||||
// We hardcode these here for now as we currently do not have any other way to filter out all the unwanted integrations.
|
||||
const AI_FOR_SOC_INTEGRATIONS = [
|
||||
'splunk', // doesnt yet exist
|
||||
'google_secops',
|
||||
'microsoft_sentinel',
|
||||
'sentinel_one',
|
||||
'crowdstrike',
|
||||
];
|
||||
import { SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS } from '../../../common/lib/search_ai_lake/integrations';
|
||||
|
||||
export interface UseFetchIntegrationsResult {
|
||||
/**
|
||||
|
@ -50,7 +42,10 @@ export const useFetchIntegrations = (): UseFetchIntegrationsResult => {
|
|||
});
|
||||
|
||||
const aiForSOCPackages: PackageListItem[] = useMemo(
|
||||
() => (allPackages?.items || []).filter((pkg) => AI_FOR_SOC_INTEGRATIONS.includes(pkg.name)),
|
||||
() =>
|
||||
(allPackages?.items || []).filter((pkg) =>
|
||||
SEARCH_AI_LAKE_ALLOWED_INTEGRATIONS.includes(pkg.name)
|
||||
),
|
||||
[allPackages]
|
||||
);
|
||||
const availablePackages: PackageListItem[] = useMemo(
|
||||
|
|
|
@ -698,13 +698,13 @@ Object {
|
|||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"link": "securitySolutionUI:configurations-ai_settings",
|
||||
"link": "securitySolutionUI:configurations-integrations",
|
||||
},
|
||||
Object {
|
||||
"link": "securitySolutionUI:configurations-basic_rules",
|
||||
},
|
||||
Object {
|
||||
"link": "securitySolutionUI:configurations-integrations",
|
||||
"link": "securitySolutionUI:configurations-ai_settings",
|
||||
},
|
||||
],
|
||||
"id": "configurations",
|
||||
|
|
|
@ -73,13 +73,13 @@ export const createAiSocNavigationTree$ = (): Rx.Observable<NavigationTreeDefini
|
|||
renderAs: 'panelOpener',
|
||||
children: [
|
||||
{
|
||||
link: securityLink(SecurityPageName.configurationsAiSettings),
|
||||
link: securityLink(SecurityPageName.configurationsIntegrations),
|
||||
},
|
||||
{
|
||||
link: securityLink(SecurityPageName.configurationsBasicRules),
|
||||
},
|
||||
{
|
||||
link: securityLink(SecurityPageName.configurationsIntegrations),
|
||||
link: securityLink(SecurityPageName.configurationsAiSettings),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -22,13 +22,13 @@ export const configurationsLink: NodeDefinition<AppDeepLinkId, string, string> =
|
|||
renderAs: 'panelOpener',
|
||||
children: [
|
||||
{
|
||||
link: `securitySolutionUI:configurations-ai_settings`,
|
||||
link: `securitySolutionUI:configurations-integrations`,
|
||||
},
|
||||
{
|
||||
link: `securitySolutionUI:configurations-basic_rules`,
|
||||
},
|
||||
{
|
||||
link: `securitySolutionUI:configurations-integrations`,
|
||||
link: `securitySolutionUI:configurations-ai_settings`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue