Skip to content
Merged

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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 @@ -258,6 +258,8 @@ export const DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL =
`${INTERNAL_DETECTION_ENGINE_URL}/rules/{ruleId}/execution/events` as const;
export const detectionEngineRuleExecutionEventsUrl = (ruleId: string) =>
`${INTERNAL_DETECTION_ENGINE_URL}/rules/${ruleId}/execution/events` as const;
export const DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL =
`${INTERNAL_DETECTION_ENGINE_URL}/fleet/integrations/installed` as const;

/**
* Telemetry detection endpoint for any previews requested of what data we are
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 semver from 'semver';
import {
RelatedIntegration,
RelatedIntegrationArray,
} from '../../../../common/detection_engine/schemas/common';

/**
* Returns and `EuiLink` that will link to a given package/integration/version page within fleet
* @param integration
* @param basePath
*/
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>
);
};

export interface InstalledIntegration extends RelatedIntegration {
targetVersion: string;
versionSatisfied?: boolean;
}

/**
* Given an array of integrations and an array of installed integrations this will return which
* integrations are `available`/`uninstalled` and which are `installed`, and also augmented with
* `targetVersion` and `versionSatisfied`
* @param integrations
* @param installedIntegrations
*/
export const getInstalledRelatedIntegrations = (
integrations: RelatedIntegrationArray,
installedIntegrations: RelatedIntegrationArray
): {
availableIntegrations: RelatedIntegrationArray;
installedRelatedIntegrations: InstalledIntegration[];
} => {
const availableIntegrations: RelatedIntegrationArray = [];
const installedRelatedIntegrations: InstalledIntegration[] = [];

integrations.forEach((i: RelatedIntegration) => {
const match = installedIntegrations.find(
(installed) => installed.package === i.package && installed?.integration === i?.integration
);
if (match != null) {
// Version check e.g. fleet match `1.2.3` satisfies rule dependency `~1.2.1`
const versionSatisfied = semver.satisfies(match.version, i.version);
installedRelatedIntegrations.push({ ...match, targetVersion: i.version, versionSatisfied });
} else {
availableIntegrations.push(i);
}
});

return {
availableIntegrations,
installedRelatedIntegrations,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* 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,
EuiBadge,
EuiPopoverTitle,
EuiFlexGroup,
EuiText,
EuiIconTip,
} from '@elastic/eui';
import styled from 'styled-components';
import { useBasePath } from '../../lib/kibana';
import { getInstalledRelatedIntegrations, getIntegrationLink } from './helpers';
import { useInstalledIntegrations } from '../../../detections/containers/detection_engine/rules/use_installed_integrations';
import type { 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 PopoverContentWrapper = styled('div')`
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 basePath = useBasePath();

const allInstalledIntegrations: RelatedIntegrationArray = data ?? [];
const { availableIntegrations, installedRelatedIntegrations } = getInstalledRelatedIntegrations(
integrations,
allInstalledIntegrations
);

const badgeTitle =
data != null
? `${installedRelatedIntegrations.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={'IntegrationsPopoverTitle'}>
{i18n.INTEGRATIONS_POPOVER_TITLE(integrations.length)}
</EuiPopoverTitle>

<PopoverContentWrapper data-test-subj={'IntegrationsPopoverContentWrapper'}>
{data != null && (
<>
<EuiText size={'s'}>
{i18n.INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED(
installedRelatedIntegrations.length
)}
</EuiText>
<ul>
{installedRelatedIntegrations.map((integration, index) => (
<IntegrationListItem key={index}>
{getIntegrationLink(integration, basePath)}
{!integration?.versionSatisfied && (
<EuiIconTip
type="alert"
content={i18n.INTEGRATIONS_INSTALLED_VERSION_TOOLTIP(
integration.version,
integration.targetVersion
)}
position="right"
/>
)}
</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>
</>
)}
</PopoverContentWrapper>
</EuiPopover>
</IntegrationsPopoverWrapper>
);
};

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

export const IntegrationsPopover =
MemoizedIntegrationsPopover as typeof IntegrationsPopoverComponent;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
Loading