Skip to content
Merged
2 changes: 2 additions & 0 deletions packages/kbn-react-field/src/field_icon/field_icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface FieldIconProps extends Omit<EuiTokenProps, 'iconType'> {
| 'geo_shape'
| 'ip'
| 'ip_range'
| 'match_only_text'
| 'murmur3'
| 'number'
| 'number_range'
Expand All @@ -45,6 +46,7 @@ export const typeToEuiIconMap: Partial<Record<string, EuiTokenProps>> = {
geo_shape: { iconType: 'tokenGeo' },
ip: { iconType: 'tokenIP' },
ip_range: { iconType: 'tokenIP' },
match_only_text: { iconType: 'tokenString' },
// is a plugin's data type https://www.elastic.co/guide/en/elasticsearch/plugins/current/mapper-murmur3-usage.html
murmur3: { iconType: 'tokenSearchType' },
number: { iconType: 'tokenNumber' },
Expand Down
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 =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@banderror -- this is as far as I went with plumbing for the installed_integrations API. Created this route, and also wired it up to to the useInstalledIntegrations hook, so should just start working if you return a RelatedIntegrationArray from this route 🙂

`${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
@@ -0,0 +1,145 @@
/*
* 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, { useState } from 'react';
import {
EuiPopover,
EuiBadgeGroup,
EuiBadge,
EuiPopoverTitle,
EuiFlexGroup,
EuiText,
} from '@elastic/eui';
import styled from 'styled-components';
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;
}

const IntegrationsPopoverWrapper = styled(EuiFlexGroup)`
width: 100%;
`;

const PopoverWrapper = styled(EuiBadgeGroup)`
max-height: 400px;
max-width: 368px;
overflow: auto;
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 integrations to display
*/
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 badgeTitle =
data != null
? `${installedIntegrations.length}/${integrations.length} ${i18n.INTEGRATIONS_BADGE}`
: `${integrations.length} ${i18n.INTEGRATIONS_BADGE}`;

return (
<IntegrationsPopoverWrapper
alignItems="center"
gutterSize="s"
data-test-subj={'IntegrationsWrapper'}
>
<EuiPopover
ownFocus
data-test-subj={'IntegrationsDisplayPopover'}
button={
<EuiBadge
iconType={'tag'}
color="hollow"
data-test-subj={'IntegrationsDisplayPopoverButton'}
onClick={() => setPopoverOpen(!isPopoverOpen)}
onClickAriaLabel={badgeTitle}
>
{badgeTitle}
</EuiBadge>
}
isOpen={isPopoverOpen}
closePopover={() => setPopoverOpen(!isPopoverOpen)}
repositionOnScroll
>
<EuiPopoverTitle data-test-subj={'IntegrationsDisplayPopoverTitle'}>
{i18n.INTEGRATIONS_POPOVER_TITLE(integrations.length)}
</EuiPopoverTitle>

<PopoverWrapper data-test-subj={'IntegrationsDisplayPopoverWrapper'}>
{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>
);
};

const MemoizedIntegrationsPopover = React.memo(IntegrationsPopoverComponent);
MemoizedIntegrationsPopover.displayName = 'IntegrationsPopover';

export const IntegrationsPopover =
MemoizedIntegrationsPopover as typeof IntegrationsPopoverComponent;
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ import {
EuiBasicTable,
EuiButton,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiText,
} from '@elastic/eui';

import styled from 'styled-components';
import { useMlHref, ML_PAGES } from '@kbn/ml-plugin/public';
import { PopoverItems } from '../../popover_items';
import { useBasePath, useKibana } from '../../../lib/kibana';
import * as i18n from './translations';
import { JobSwitch } from './job_switch';
Expand Down Expand Up @@ -82,16 +81,24 @@ const getJobsTableColumns = (
},
{
name: i18n.COLUMN_GROUPS,
render: ({ groups }: SecurityJob) => (
<EuiFlexGroup wrap responsive={true} gutterSize="xs">
{groups.map((group) => (
<EuiFlexItem grow={false} key={group}>
<EuiBadge color={'hollow'}>{group}</EuiBadge>
</EuiFlexItem>
))}
</EuiFlexGroup>
),
width: '140px',
render: ({ groups }: SecurityJob) => {
const renderItem = (group: string, i: number) => (
<EuiBadge color="hollow" key={`${group}-${i}`} data-test-subj="group">
{group}
</EuiBadge>
);

return (
<PopoverItems
items={groups}
numberOfItemsToDisplay={0}
popoverButtonTitle={`${groups.length} Groups`}
renderItem={renderItem}
dataTestPrefix="groups"
/>
Comment on lines +92 to +98
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't mind this... 😅

I was testing #131166 the other day and had it with the badge overflow. Since in doing this PR I learned of the PopoverItems component I figured there was a nice quick fix here:

cc @randomuserid

);
},
width: '80px',
},

{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,11 @@ const PopoverItemsComponent = <T extends unknown>({

return (
<PopoverItemsWrapper alignItems="center" gutterSize="s" data-test-subj={dataTestPrefix}>
<EuiFlexItem grow={1} className="eui-textTruncate">
<OverflowList items={items.slice(0, numberOfItemsToDisplay)} />
</EuiFlexItem>
{numberOfItemsToDisplay !== 0 && (
<EuiFlexItem grow={1} className="eui-textTruncate">
<OverflowList items={items.slice(0, numberOfItemsToDisplay)} />
</EuiFlexItem>
)}
Comment on lines +79 to +83
Copy link
Member Author

@spong spong May 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spacing fix from leftover element when displaying empty items. Current implementation/use on Rules Table for Tags didn't exercise this path.

<EuiPopover
ownFocus
data-test-subj={`${dataTestPrefix}DisplayPopover`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ import {
EuiText,
EuiIcon,
EuiToolTip,
EuiFlexGrid,
} from '@elastic/eui';
import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils';
import { capitalize } from 'lodash';

import { isEmpty } from 'lodash/fp';
import React from 'react';
import styled from 'styled-components';
import { FieldIcon } from '@kbn/react-field';

import { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types';
import { getDisplayValueFromFilter } from '@kbn/data-plugin/public';
Expand All @@ -30,7 +33,11 @@ import { MATCHES, AND, OR } from '../../../../common/components/threat_match/tra
import { assertUnreachable } from '../../../../../common/utility_types';
import * as i18nSeverity from '../severity_mapping/translations';
import * as i18nRiskScore from '../risk_score_mapping/translations';
import { Threshold } from '../../../../../common/detection_engine/schemas/common/schemas';
import type {
RelatedIntegrationArray,
RequiredFieldArray,
Threshold,
} from '../../../../../common/detection_engine/schemas/common';
import {
subtechniquesOptions,
tacticsOptions,
Expand Down Expand Up @@ -507,3 +514,71 @@ export const buildThreatMappingDescription = (
},
];
};

export const buildRelatedIntegrationsDescription = (
label: string,
relatedIntegrations: RelatedIntegrationArray
): ListItems[] => {
const badgeInstalledColor = '#E0E5EE'; // 'subdued' not working?
const badgeUninstalledColor = 'accent';
const basePath = 'http://localhost:5601/kbn'; // const basePath = useBasePath();
const installedText = 'Installed';
const uninstalledText = 'Uninstalled';
const installedPackages = ['aws']; // TODO: Use hook const { data } = useInstalledIntegrations({ packages: [] });

return relatedIntegrations.map((rI, index) => {
const isInstalled = installedPackages.includes(rI.package);
const badgeColor = isInstalled ? badgeInstalledColor : badgeUninstalledColor;
const badgeText = isInstalled ? installedText : uninstalledText;
const integrationURL = `${basePath}/app/integrations/detail/${rI.package}-${
rI.version
}/overview${rI.integration ? `?integration=${rI.integration}` : ''}`;

return {
title: index === 0 ? label : '',
description: (
<>
<EuiLink href={integrationURL} target="_blank">
{`${capitalize(rI.package)} ${capitalize(rI.integration)}`}
</EuiLink>{' '}
<EuiBadge color={badgeColor}>{badgeText}</EuiBadge>
</>
),
};
});
};

const FieldTypeText = styled(EuiText)`
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
font-family: ${({ theme }) => theme.eui.euiCodeFontFamily};
display: inline;
`;

export const buildRequiredFieldsDescription = (
label: string,
requiredFields: RequiredFieldArray
): ListItems[] => {
return [
{
title: label,
description: (
<EuiFlexGrid gutterSize={'s'}>
{requiredFields.map((rF, index) => (
<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>
))}
</EuiFlexGrid>
),
},
];
};
Loading