Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6491a60
base setup for configurations integrations browse and installed pages
kgeller Apr 2, 2025
b2a4963
updating note to myself
kgeller Apr 3, 2025
063a924
sorting and fixing installed only show allowed
kgeller Apr 3, 2025
aabac67
refactor to only have one page that we just send props in to
kgeller Apr 3, 2025
fe11da0
sorting by id
kgeller Apr 3, 2025
cd355dc
splunk too
kgeller Apr 3, 2025
9aa976c
Merge branch 'master' into add-integrations-pages
kgeller Apr 7, 2025
7973fa0
added relevant tests and jest config for configurations
kgeller Apr 7, 2025
3ff7756
updating loading skeleton to match figma, cleaning up code
kgeller Apr 8, 2025
687c94c
cleanup
kgeller Apr 8, 2025
b47a404
fixing link order to match tabs
kgeller Apr 9, 2025
54ebfb4
using lodash noop
kgeller Apr 9, 2025
5c5d205
move allowlist to common/constants
kgeller Apr 9, 2025
bede677
Merge branch 'master' into add-integrations-pages
kgeller Apr 10, 2025
02d2100
cleanup
kgeller Apr 10, 2025
483708c
fixing tests
kgeller Apr 10, 2025
c38700f
moving from configurations subplugin to common/lib/search_ai_lake
kgeller Apr 11, 2025
b27efa3
updating alerts to use searchailake constants allowlist
kgeller Apr 11, 2025
d6a19c4
rename
kgeller Apr 11, 2025
0343130
Merge branch 'main' into add-integrations-pages
kgeller Apr 11, 2025
4c2ea84
Merge branch 'main' into add-integrations-pages
kgeller Apr 14, 2025
97adffb
moving non-shared pieces back to configurations subplugin; keeping sh…
kgeller Apr 14, 2025
49dc58a
moving hook to shared
kgeller Apr 14, 2025
fcf8abd
Merge branch 'main' into add-integrations-pages
kgeller Apr 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down Expand Up @@ -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};
Expand All @@ -230,15 +232,15 @@ export function PackageCard({
layout="horizontal"
title={title || ''}
titleSize="xs"
description={description}
description={showDescription ? description : ''}
hasBorder
icon={
<CardIcon
icons={icons}
packageName={name}
integrationName={integration}
version={version}
size="xl"
size={showDescription ? 'xl' : 'xxl'}
/>
}
onClick={onClickProp ?? onCardClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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',
];
Comment thread
semd marked this conversation as resolved.
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 };
};
Original file line number Diff line number Diff line change
@@ -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',
];
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ export enum ConfigurationTabs {
basicRules = 'basic_rules',
aiSettings = 'ai_settings',
}

export enum IntegrationsFacets {
available = 'browse',
installed = 'installed',
}
Original file line number Diff line number Diff line change
@@ -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'),
};
Loading
Loading