Skip to content

Commit

Permalink
Adds hook for fetching installedIntegrations and cleans up UI components
Browse files Browse the repository at this point in the history
  • Loading branch information
spong committed May 21, 2022
1 parent 0b48577 commit b0247a9
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 44 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ export const DETECTION_ENGINE_PREPACKAGED_URL =
`${DETECTION_ENGINE_RULES_URL}/prepackaged` as const;
export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges` as const;
export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index` as const;
export const DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL =
`${DETECTION_ENGINE_URL}/installed_integrations` as const;
export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags` as const;
export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL =
`${DETECTION_ENGINE_RULES_URL}/prepackaged/_status` as const;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 { EuiLink } from '@elastic/eui';
import { capitalize } from 'lodash';
import React from 'react';
import { RelatedIntegration } from '../../../../common/detection_engine/schemas/common';

export const getIntegrationLink = (integration: RelatedIntegration, basePath: string) => {
const integrationURL = `${basePath}/app/integrations/detail/${integration.package}-${
integration.version
}/overview${integration.integration ? `?integration=${integration.integration}` : ''}`;
return (
<EuiLink href={integrationURL} target="_blank">
{`${capitalize(integration.package)} ${capitalize(integration.integration)}`}
</EuiLink>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,20 @@ import {
EuiPopoverTitle,
EuiFlexGroup,
EuiText,
EuiLink,
} from '@elastic/eui';
import styled from 'styled-components';
import type { RelatedIntegrationArray } from '../../../../common/detection_engine/schemas/common';
import { useBasePath } from '../../lib/kibana';
import { getIntegrationLink } from './helpers';
import { useInstalledIntegrations } from '../../../detections/containers/detection_engine/rules/use_installed_integrations';
import type {
RelatedIntegration,
RelatedIntegrationArray,
} from '../../../../common/detection_engine/schemas/common';

import * as i18n from '../../../detections/pages/detection_engine/rules/translations';

export interface IntegrationsPopoverProps {
integrations: RelatedIntegrationArray;
installedIntegrations: RelatedIntegrationArray;
}

const IntegrationsPopoverWrapper = styled(EuiFlexGroup)`
Expand All @@ -36,18 +40,40 @@ const PopoverWrapper = styled(EuiBadgeGroup)`
line-height: ${({ theme }) => theme.eui.euiLineHeight};
`;

const IntegrationListItem = styled('li')`
list-style-type: disc;
margin-left: 25px;
`;
/**
* Component to render installed and available integrations
* @param integrations - array of items to render
* @param installedIntegrations - array of items to render
* @param integrations - array of integrations to display
*/
const IntegrationsPopoverComponent = ({
integrations,
installedIntegrations,
}: IntegrationsPopoverProps) => {
const IntegrationsPopoverComponent = ({ integrations }: IntegrationsPopoverProps) => {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const { data } = useInstalledIntegrations({ packages: [] });
// const data = undefined; // To test with installed_integrations endpoint not implemented
const basePath = useBasePath();

const allInstalledIntegrations: RelatedIntegrationArray = data ?? [];
const availableIntegrations: RelatedIntegrationArray = [];
const installedIntegrations: RelatedIntegrationArray = [];

integrations.forEach((i: RelatedIntegration) => {
const match = allInstalledIntegrations.find(
(installed) => installed.package === i.package && installed?.integration === i?.integration
);
if (match != null) {
// TODO: Do version check
installedIntegrations.push(match);
} else {
availableIntegrations.push(i);
}
});

const integrationsTitle = `${installedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}`;
const badgeTitle =
data != null
? `${installedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}`
: `${integrations.length} ${i18n.INTEGRATIONS_BADGE}`;

return (
<IntegrationsPopoverWrapper
Expand All @@ -64,35 +90,48 @@ const IntegrationsPopoverComponent = ({
color="hollow"
data-test-subj={'IntegrationsDisplayPopoverButton'}
onClick={() => setPopoverOpen(!isPopoverOpen)}
onClickAriaLabel={integrationsTitle}
onClickAriaLabel={badgeTitle}
>
{integrationsTitle}
{badgeTitle}
</EuiBadge>
}
isOpen={isPopoverOpen}
closePopover={() => setPopoverOpen(!isPopoverOpen)}
repositionOnScroll
>
<EuiPopoverTitle data-test-subj={'IntegrationsDisplayPopoverTitle'}>
{i18n.INTEGRATIONS_POPOVER_TITLE(3)}
{i18n.INTEGRATIONS_POPOVER_TITLE(integrations.length)}
</EuiPopoverTitle>

<PopoverWrapper data-test-subj={'IntegrationsDisplayPopoverWrapper'}>
<EuiText size={'s'}>{i18n.INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED(1)}</EuiText>
<EuiLink href={'integrationURL'} target="_blank">
{'AWS CloudTrail'}
</EuiLink>
<EuiText size={'s'}>{i18n.INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED(2)}</EuiText>
<div>
<EuiLink href={'integrationURL'} target="_blank">
{'Endpoint Security'}
</EuiLink>
</div>
<div>
<EuiLink href={'integrationURL'} target="_blank">
{'\nModSecurity Audit'}
</EuiLink>
</div>
{data != null && (
<>
<EuiText size={'s'}>
{i18n.INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED(installedIntegrations.length)}
</EuiText>
<ul>
{installedIntegrations.map((integration, index) => (
<IntegrationListItem key={index}>
{getIntegrationLink(integration, basePath)}
</IntegrationListItem>
))}
</ul>
</>
)}
{availableIntegrations.length > 0 && (
<>
<EuiText size={'s'}>
{i18n.INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED(availableIntegrations.length)}
</EuiText>
<ul>
{availableIntegrations.map((integration, index) => (
<IntegrationListItem key={index}>
{getIntegrationLink(integration, basePath)}
</IntegrationListItem>
))}
</ul>
</>
)}
</PopoverWrapper>
</EuiPopover>
</IntegrationsPopoverWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
EuiText,
EuiIcon,
EuiToolTip,
EuiFlexGrid,
} from '@elastic/eui';
import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils';
import { capitalize } from 'lodash';
Expand Down Expand Up @@ -520,10 +521,10 @@ export const buildRelatedIntegrationsDescription = (
): ListItems[] => {
const badgeInstalledColor = '#E0E5EE'; // 'subdued' not working?
const badgeUninstalledColor = 'accent';
const basePath = 'http://localhost:5601/kbn'; // const { basePath } = useBasePath();
const basePath = 'http://localhost:5601/kbn'; // const basePath = useBasePath();
const installedText = 'Installed';
const uninstalledText = 'Uninstalled';
const installedPackages = ['aws'];
const installedPackages = ['aws']; // TODO: Use hook const { data } = useInstalledIntegrations({ packages: [] });

return relatedIntegrations.map((rI, index) => {
const isInstalled = installedPackages.includes(rI.package);
Expand All @@ -538,9 +539,7 @@ export const buildRelatedIntegrationsDescription = (
description: (
<>
<EuiLink href={integrationURL} target="_blank">
{rI.integration
? `${capitalize(rI.integration)} ${capitalize(rI.integration)}`
: capitalize(rI.package)}
{`${capitalize(rI.package)} ${capitalize(rI.integration)}`}
</EuiLink>{' '}
<EuiBadge color={badgeColor}>{badgeText}</EuiBadge>
</>
Expand All @@ -552,6 +551,7 @@ export const buildRelatedIntegrationsDescription = (
const FieldTypeText = styled(EuiText)`
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
font-family: ${({ theme }) => theme.eui.euiCodeFontFamily};
display: inline;
`;

export const buildRequiredFieldsDescription = (
Expand All @@ -562,15 +562,22 @@ export const buildRequiredFieldsDescription = (
{
title: label,
description: (
<FieldTypeText grow={false} size={'s'}>
<EuiFlexGrid gutterSize={'s'}>
{requiredFields.map((rF, index) => (
<>
<FieldIcon data-test-subj="field-type-icon" type={rF.type} />
{` ${rF.name}`}
{index + 1 !== requiredFields.length && <>{', '}</>}
</>
<EuiFlexItem grow={false}>
<EuiFlexGroup alignItems="center" gutterSize={'xs'}>
<EuiFlexItem grow={false}>
<FieldIcon data-test-subj="field-type-icon" type={rF.type} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<FieldTypeText grow={false} size={'s'}>
{` ${rF.name}${index + 1 !== requiredFields.length ? ', ' : ''}`}
</FieldTypeText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
))}
</FieldTypeText>
</EuiFlexGrid>
),
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import {
DETECTION_ENGINE_RULES_BULK_ACTION,
DETECTION_ENGINE_RULES_PREVIEW,
detectionEngineRuleExecutionEventsUrl,
DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL,
} from '../../../../../common/constants';
import {
AggregateRuleExecutionEvent,
BulkAction,
RelatedIntegrationArray,
RuleExecutionStatus,
} from '../../../../../common/detection_engine/schemas/common';
import {
Expand Down Expand Up @@ -408,3 +410,29 @@ export const getPrePackagedRulesStatus = async ({
signal,
}
);

/**
* Fetch all installed integrations
*
* @param packages array of packages to filter for
* @param signal to cancel request
*
* @throws An error if response is not OK
*/
export const fetchInstalledIntegrations = async ({
packages,
signal,
}: {
packages?: string[];
signal?: AbortSignal;
}): Promise<RelatedIntegrationArray> =>
KibanaServices.get().http.fetch<RelatedIntegrationArray>(
DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL,
{
method: 'GET',
query: {
packages: packages?.sort()?.join(','),
},
signal,
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,10 @@ export const RULE_EXECUTION_EVENTS_FETCH_FAILURE = i18n.translate(
defaultMessage: 'Failed to fetch rule execution events',
}
);

export const INSTALLED_INTEGRATIONS_FETCH_FAILURE = i18n.translate(
'xpack.securitySolution.containers.detectionEngine.installedIntegrationsFetchFailDescription',
{
defaultMessage: 'Failed to fetch installed integrations',
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 { useQuery } from 'react-query';
import { RelatedIntegrationArray } from '../../../../../common/detection_engine/schemas/common';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import * as i18n from './translations';

export interface UseInstalledIntegrationsArgs {
packages?: string[];
}

export const useInstalledIntegrations = ({ packages }: UseInstalledIntegrationsArgs) => {
const { addError } = useAppToasts();

return useQuery<RelatedIntegrationArray>(
[
'installedIntegrations',
{
packages,
},
],
async ({ signal }) => {
// Mock data
const mockInstalledIntegrations = [
{
package: 'system',
version: '1.6.4',
},
// {
// package: 'aws',
// integration: 'cloudtrail',
// version: '1.11.0',
// },
];
return mockInstalledIntegrations;

// Or fetch from new API
// return fetchInstalledIntegrations({
// packages,
// signal,
// });
},
{
keepPreviousData: true,
onError: (e) => {
addError(e, { title: i18n.INSTALLED_INTEGRATIONS_FETCH_FAILURE });
},
}
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ const INTEGRATIONS_COLUMN: TableColumn = {
return null;
}

return <IntegrationsPopover integrations={integrations} installedIntegrations={[]} />;
return <IntegrationsPopover integrations={integrations} />;
},
width: '143px',
truncateText: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,7 @@ export const INTEGRATIONS_POPOVER_TITLE = (integrationsCount: number) =>
{
values: { integrationsCount },
defaultMessage:
'You have [{integrationsCount}] related {integrationsCount, plural, =1 {# integration} other {# integrations}} to your prebuilt rule',
'You have [{integrationsCount}] related {integrationsCount, plural, =1 {integration} other {integrations}} to your prebuilt rule',
}
);

Expand All @@ -1109,7 +1109,7 @@ export const INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED = (installedCount: numbe
{
values: { installedCount },
defaultMessage:
'You have [{installedCount}] related {installedCount, plural, =1 {# integration} other {# integrations}} installed, click the link below to view the integration:',
'You have [{installedCount}] related {installedCount, plural, =1 {integration} other {integrations}} installed, click the link below to view the integration:',
}
);

Expand All @@ -1119,6 +1119,6 @@ export const INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED = (uninstalledCount: n
{
values: { uninstalledCount },
defaultMessage:
'You have [{uninstalledCount}] related {uninstalledCount, plural, =1 {# integration} other {# integrations}} uninstalled, click the link to add integration:',
'You have [{uninstalledCount}] related {uninstalledCount, plural, =1 {integration} other {integrations}} uninstalled, click the link to add integration:',
}
);

0 comments on commit b0247a9

Please sign in to comment.