mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution][Detections] Related Integrations & Required Fields Feedback & Fixes (#133050)
## Summary Addressing the following feedback from https://github.com/elastic/kibana/pull/132847: - [X] On the Rule Management page package name is used instead of package title when the package has only 1 integration: - [X] move integrations_popover to `related_integrations` directory - [X] update useInstalledIntegrations to always return array of installedIntegration - [X] move useInstalledIntegrations to `related_integrations` directory - [X] Slight update to copy in Rules Table popover - [X] Ok to use Rule Details UI within Rules Table popover content - [X] Sort integrations alphabetically - [X] Update left padding on version mis-match tooltip - [X] https://github.com/elastic/kibana/issues/133291 - [X] https://github.com/elastic/kibana/issues/133269 - [X] Add Kibana Advanced Setting for disabling integrations badge on Rules Table <p align="center"> <img width="500" src="https://user-images.githubusercontent.com/2946766/171750790-ffa2e3ef-dd7a-499c-9b08-89bafc06dd50.png" /> </p> - [ ] Adds tests - [x] `useInstalledIntegrations` hook - [X] relatedIntegrations utils - [x] IntegrationDescription - [ ] Add loaders where necessary since there can now be API delay - May hold off on loaders as transition from no installed integrations -> installed integrations isn't too bad as-is ##### Updated integrations popover content on Rules Table to match Rule Details design <p align="center"> <img width="500" src="https://user-images.githubusercontent.com/2946766/172263941-3e948b41-7ef7-4281-8354-57e77ddeb433.png" /> </p> In discussions with @banderror reviewing the different integration states (uninstalled, installed, enabled, and agents enrolled), we are now capturing the distinction between `Installed` and `Enabled` so that we don't confuse users when a package is installed but the integration isn't enabled/configured. I also added tooltips for clarifying each state and what action the user should perform to ensure compatibility. In collab with @yiyangliu9286 @jethr0null (comments below) -- we've consolidated to a single `Installed: enabled` badge, and updated `Uninstalled` to `Not installed` as well. ##### Tooltips <details><summary>Not installed</summary> <p align="center"> <img width="500" src="https://user-images.githubusercontent.com/2946766/172264210-00064485-2df9-408e-953b-9294f16dedf2.png" /> </p> </details> <details><summary>Installed</summary> <p align="center"> <img width="500" src="https://user-images.githubusercontent.com/2946766/172263672-67b641cd-5895-464a-8897-f26bd0a61073.png" /> </p> </details> <details><summary>Installed: enabled</summary> <p align="center"> <img width="500" src="https://user-images.githubusercontent.com/2946766/172263783-563ea48d-c96c-4519-87b4-7076582f5da2.png" /> </p> </details> ### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [X] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - Collaborating with docs teams on this dedicated docs issue: https://github.com/elastic/security-docs/issues/2015 - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [X] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
This commit is contained in:
parent
e09ff34b0a
commit
7bfcb52901
23 changed files with 1003 additions and 433 deletions
|
@ -449,6 +449,9 @@ events.
|
|||
[[securitysolution-threatindices]]`securitySolution:defaultThreatIndex`::
|
||||
A comma-delimited list of Threat Intelligence indices from which the {security-app} collects indicators.
|
||||
|
||||
[[securitysolution-enableCcsWarning]]`securitySolution:enableCcsWarning`:: Enables
|
||||
privilege check warnings in rules for CCS indices.
|
||||
|
||||
[[securitysolution-enablenewsfeed]]`securitySolution:enableNewsFeed`:: Enables
|
||||
the security news feed on the Security *Overview* page.
|
||||
|
||||
|
@ -464,7 +467,10 @@ The URL from which the security news feed content is retrieved.
|
|||
The default refresh interval for the Security time filter, in milliseconds.
|
||||
|
||||
[[security-solution-rules-table-refresh]]`securitySolution:rulesTableRefresh`::
|
||||
The default period of time in the Security time filter.
|
||||
Enables auto refresh on the rules and monitoring tables, in milliseconds.
|
||||
|
||||
[[securitySolution-showRelatedIntegrations]]`securitySolution:showRelatedIntegrations`::
|
||||
Shows related integrations on the rules and monitoring tables.
|
||||
|
||||
[[securitysolution-timedefaults]]`securitySolution:timeDefaults`::
|
||||
The default period of time in the Security time filter.
|
||||
|
|
|
@ -218,6 +218,10 @@ export const IP_REPUTATION_LINKS_SETTING_DEFAULT = `[
|
|||
{ "name": "talosIntelligence.com", "url_template": "https://talosintelligence.com/reputation_center/lookup?search={{ip}}" }
|
||||
]`;
|
||||
|
||||
/** This Kibana Advanced Setting shows related integrations on the Rules Table */
|
||||
export const SHOW_RELATED_INTEGRATIONS_SETTING =
|
||||
'securitySolution:showRelatedIntegrations' as const;
|
||||
|
||||
/**
|
||||
* Id for the notifications alerting type
|
||||
* @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
export * from './error_schema';
|
||||
export * from './get_installed_integrations_response_schema';
|
||||
export * from './get_rule_execution_events_response';
|
||||
export * from './import_rules_schema';
|
||||
export * from './prepackaged_rules_schema';
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
InstalledIntegration,
|
||||
InstalledIntegrationArray,
|
||||
RelatedIntegration,
|
||||
RelatedIntegrationArray,
|
||||
} from '../../../../common/detection_engine/schemas/common';
|
||||
|
||||
/**
|
||||
* Returns an `EuiLink` that will link to a given package/integration/version page within fleet
|
||||
* TODO: Add `title` to RelatedIntegration so we can accurately display the integration pretty name
|
||||
*
|
||||
* @param integration either RelatedIntegration or InstalledIntegration
|
||||
* @param basePath kbn basepath for composing the fleet URL
|
||||
*/
|
||||
export const getIntegrationLink = (
|
||||
integration: RelatedIntegration | InstalledIntegration,
|
||||
basePath: string
|
||||
) => {
|
||||
let packageName: string;
|
||||
let integrationName: string | undefined;
|
||||
let integrationTitle: string;
|
||||
let version: string | null;
|
||||
|
||||
// InstalledIntegration
|
||||
if ('package_name' in integration) {
|
||||
packageName = integration.package_name;
|
||||
integrationName = integration.integration_name;
|
||||
integrationTitle = integration.integration_title ?? integration.package_name;
|
||||
version = integration.package_version;
|
||||
} else {
|
||||
// RelatedIntegration
|
||||
packageName = integration.package;
|
||||
integrationName = integration.integration;
|
||||
integrationTitle = `${capitalize(integration.package)} ${capitalize(integration.integration)}`;
|
||||
version = semver.valid(semver.coerce(integration.version));
|
||||
}
|
||||
|
||||
const integrationURL =
|
||||
version != null
|
||||
? `${basePath}/app/integrations/detail/${packageName}-${version}/overview${
|
||||
integrationName ? `?integration=${integrationName}` : ''
|
||||
}`
|
||||
: `${basePath}/app/integrations/detail/${packageName}`;
|
||||
return (
|
||||
<EuiLink href={integrationURL} target="_blank">
|
||||
{integrationTitle}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
export interface InstalledIntegrationAugmented extends InstalledIntegration {
|
||||
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: InstalledIntegrationArray
|
||||
): {
|
||||
availableIntegrations: RelatedIntegrationArray;
|
||||
installedRelatedIntegrations: InstalledIntegrationAugmented[];
|
||||
} => {
|
||||
const availableIntegrations: RelatedIntegrationArray = [];
|
||||
const installedRelatedIntegrations: InstalledIntegrationAugmented[] = [];
|
||||
|
||||
integrations.forEach((i: RelatedIntegration) => {
|
||||
const match = installedIntegrations.find(
|
||||
(installed) =>
|
||||
installed.package_name === i.package && installed?.integration_name === 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.package_version, i.version);
|
||||
installedRelatedIntegrations.push({ ...match, targetVersion: i.version, versionSatisfied });
|
||||
} else {
|
||||
availableIntegrations.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
availableIntegrations,
|
||||
installedRelatedIntegrations,
|
||||
};
|
||||
};
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
* 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 { InstalledIntegrationArray } from '../../../../common/detection_engine/schemas/common';
|
||||
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: InstalledIntegrationArray = data?.installed_integrations ?? [];
|
||||
const { availableIntegrations, installedRelatedIntegrations } = getInstalledRelatedIntegrations(
|
||||
integrations,
|
||||
allInstalledIntegrations
|
||||
);
|
||||
|
||||
// TODO: Add loader to installed integrations value
|
||||
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={'package'}
|
||||
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.package_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;
|
|
@ -20,6 +20,8 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { ALERT_RISK_SCORE } from '@kbn/rule-data-utils';
|
||||
|
||||
import { castEsToKbnFieldTypeName } from '@kbn/field-types';
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
@ -556,7 +558,7 @@ export const buildRequiredFieldsDescription = (
|
|||
label: string,
|
||||
requiredFields: RequiredFieldArray
|
||||
): ListItems[] => {
|
||||
if (requiredFields == null) {
|
||||
if (isEmpty(requiredFields)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -569,7 +571,11 @@ export const buildRequiredFieldsDescription = (
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize={'xs'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldIcon data-test-subj="field-type-icon" type={rF.type} />
|
||||
<FieldIcon
|
||||
data-test-subj="field-type-icon"
|
||||
type={castEsToKbnFieldTypeName(rF.type)}
|
||||
label={rF.type}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldTypeText grow={false} size={'s'}>
|
||||
|
|
|
@ -13,7 +13,7 @@ import styled from 'styled-components';
|
|||
import { ThreatMapping, Threats, Type } from '@kbn/securitysolution-io-ts-alerting-types';
|
||||
import { DataViewBase, Filter, FilterStateStore } from '@kbn/es-query';
|
||||
import { FilterManager } from '@kbn/data-plugin/public';
|
||||
import { buildRelatedIntegrationsDescription } from './required_integrations_description';
|
||||
import { buildRelatedIntegrationsDescription } from '../related_integrations/integrations_description';
|
||||
import type {
|
||||
RelatedIntegrationArray,
|
||||
RequiredFieldArray,
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
* 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 styled from 'styled-components';
|
||||
import { EuiBadge, EuiIconTip } from '@elastic/eui';
|
||||
import { INTEGRATIONS_INSTALLED_VERSION_TOOLTIP } from '../../../pages/detection_engine/rules/translations';
|
||||
import { useInstalledIntegrations } from '../../../containers/detection_engine/rules/use_installed_integrations';
|
||||
import {
|
||||
getInstalledRelatedIntegrations,
|
||||
getIntegrationLink,
|
||||
} from '../../../../common/components/integrations_popover/helpers';
|
||||
|
||||
import {
|
||||
InstalledIntegrationArray,
|
||||
RelatedIntegration,
|
||||
RelatedIntegrationArray,
|
||||
} from '../../../../../common/detection_engine/schemas/common';
|
||||
import { useBasePath } from '../../../../common/lib/kibana';
|
||||
import { ListItems } from './types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const IntegrationDescriptionComponent: React.FC<{ integration: RelatedIntegration }> = ({
|
||||
integration,
|
||||
}) => {
|
||||
const basePath = useBasePath();
|
||||
const badgeInstalledColor = '#E0E5EE';
|
||||
const badgeUninstalledColor = 'accent';
|
||||
const { data } = useInstalledIntegrations({ packages: [] });
|
||||
|
||||
const allInstalledIntegrations: InstalledIntegrationArray = data?.installed_integrations ?? [];
|
||||
const { availableIntegrations, installedRelatedIntegrations } = getInstalledRelatedIntegrations(
|
||||
[integration],
|
||||
allInstalledIntegrations
|
||||
);
|
||||
|
||||
if (availableIntegrations.length > 0) {
|
||||
return (
|
||||
<Wrapper>
|
||||
{getIntegrationLink(integration, basePath)}{' '}
|
||||
{data != null && (
|
||||
<EuiBadge color={badgeUninstalledColor}>{i18n.RELATED_INTEGRATIONS_UNINSTALLED}</EuiBadge>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
} else if (installedRelatedIntegrations.length > 0) {
|
||||
return (
|
||||
<Wrapper>
|
||||
{getIntegrationLink(integration, basePath)}{' '}
|
||||
<EuiBadge color={badgeInstalledColor}>{i18n.RELATED_INTEGRATIONS_INSTALLED}</EuiBadge>
|
||||
{!installedRelatedIntegrations[0]?.versionSatisfied && (
|
||||
<EuiIconTip
|
||||
type="alert"
|
||||
content={INTEGRATIONS_INSTALLED_VERSION_TOOLTIP(
|
||||
installedRelatedIntegrations[0]?.package_version,
|
||||
installedRelatedIntegrations[0]?.targetVersion
|
||||
)}
|
||||
position="right"
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
export const IntegrationDescription = React.memo(IntegrationDescriptionComponent);
|
||||
|
||||
const RelatedIntegrationsDescription: React.FC<{ integrations: RelatedIntegrationArray }> = ({
|
||||
integrations,
|
||||
}) => (
|
||||
<>
|
||||
{integrations?.map((integration, index) => (
|
||||
<IntegrationDescription key={`${integration.package}-${index}`} integration={integration} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
export const buildRelatedIntegrationsDescription = (
|
||||
label: string,
|
||||
relatedIntegrations: RelatedIntegrationArray
|
||||
): ListItems[] => [
|
||||
{
|
||||
title: label,
|
||||
description: <RelatedIntegrationsDescription integrations={relatedIntegrations} />,
|
||||
},
|
||||
];
|
|
@ -118,17 +118,3 @@ export const EQL_TIMESTAMP_FIELD_LABEL = i18n.translate(
|
|||
defaultMessage: 'Timestamp field',
|
||||
}
|
||||
);
|
||||
|
||||
export const RELATED_INTEGRATIONS_INSTALLED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrationsInstalledDescription',
|
||||
{
|
||||
defaultMessage: 'Installed',
|
||||
}
|
||||
);
|
||||
|
||||
export const RELATED_INTEGRATIONS_UNINSTALLED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDescription.relatedIntegrationsUninstalledDescription',
|
||||
{
|
||||
defaultMessage: 'Uninstalled',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { installedIntegrationsBase, relatedIntegrations } from '../mock';
|
||||
import { useInstalledIntegrations } from '../use_installed_integrations';
|
||||
import { getInstalledRelatedIntegrations } from '../utils';
|
||||
import { IntegrationDescription } from '.';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
jest.mock('../use_installed_integrations');
|
||||
|
||||
const mockUseInstalledIntegrations = useInstalledIntegrations as jest.Mock;
|
||||
mockUseInstalledIntegrations.mockReturnValue({
|
||||
data: installedIntegrationsBase,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
});
|
||||
|
||||
describe('IntegrationDescription', () => {
|
||||
test('Shows total events returned', () => {
|
||||
const { data: allInstalledIntegrations } = useInstalledIntegrations({ packages: [] });
|
||||
|
||||
const integrationDetails = getInstalledRelatedIntegrations(
|
||||
relatedIntegrations,
|
||||
allInstalledIntegrations
|
||||
);
|
||||
render(<IntegrationDescription integration={integrationDetails[0]} />);
|
||||
expect(screen.getByTestId('integrationLink')).toHaveTextContent('Aws Cloudtrail');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 styled from 'styled-components';
|
||||
import { EuiBadge, EuiIconTip, EuiToolTip } from '@elastic/eui';
|
||||
import { useInstalledIntegrations } from '../use_installed_integrations';
|
||||
import { getInstalledRelatedIntegrations, getIntegrationLink, IntegrationDetails } from '../utils';
|
||||
|
||||
import { RelatedIntegrationArray } from '../../../../../../common/detection_engine/schemas/common';
|
||||
import { useBasePath } from '../../../../../common/lib/kibana';
|
||||
import { ListItems } from '../../description_step/types';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const PaddedBadge = styled(EuiBadge)`
|
||||
margin-left: 5px;
|
||||
`;
|
||||
|
||||
const VersionWarningIconContainer = styled.span`
|
||||
margin-left: 5px;
|
||||
`;
|
||||
|
||||
export const IntegrationDescriptionComponent: React.FC<{ integration: IntegrationDetails }> = ({
|
||||
integration,
|
||||
}) => {
|
||||
const basePath = useBasePath();
|
||||
const badgeInstalledColor = 'success';
|
||||
const badgeUninstalledColor = '#E0E5EE';
|
||||
const badgeColor = integration.is_installed ? badgeInstalledColor : badgeUninstalledColor;
|
||||
const badgeTooltip = integration.is_installed
|
||||
? integration.is_enabled
|
||||
? i18n.INTEGRATIONS_ENABLED_TOOLTIP
|
||||
: i18n.INTEGRATIONS_INSTALLED_TOOLTIP
|
||||
: i18n.INTEGRATIONS_UNINSTALLED_TOOLTIP;
|
||||
const badgeText = integration.is_installed
|
||||
? integration.is_enabled
|
||||
? i18n.INTEGRATIONS_ENABLED
|
||||
: i18n.INTEGRATIONS_INSTALLED
|
||||
: i18n.INTEGRATIONS_UNINSTALLED;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{getIntegrationLink(integration, basePath)}{' '}
|
||||
<EuiToolTip content={badgeTooltip}>
|
||||
<PaddedBadge color={badgeColor}>{badgeText}</PaddedBadge>
|
||||
</EuiToolTip>
|
||||
{integration.is_installed && !integration.version_satisfied && (
|
||||
<VersionWarningIconContainer>
|
||||
<EuiIconTip
|
||||
type={'alert'}
|
||||
color={'warning'}
|
||||
content={i18n.INTEGRATIONS_INSTALLED_VERSION_TOOLTIP(
|
||||
integration.package_version,
|
||||
integration.target_version
|
||||
)}
|
||||
/>
|
||||
</VersionWarningIconContainer>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const IntegrationDescription = React.memo(IntegrationDescriptionComponent);
|
||||
|
||||
export const RelatedIntegrationsDescription: React.FC<{
|
||||
integrations: RelatedIntegrationArray;
|
||||
}> = ({ integrations }) => {
|
||||
const { data: allInstalledIntegrations } = useInstalledIntegrations({ packages: [] });
|
||||
|
||||
const integrationDetails = getInstalledRelatedIntegrations(
|
||||
integrations,
|
||||
allInstalledIntegrations
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{integrationDetails.map((integration, index) => (
|
||||
<IntegrationDescription
|
||||
key={`${integration.package_name}-${index}`}
|
||||
integration={integration}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const buildRelatedIntegrationsDescription = (
|
||||
label: string,
|
||||
relatedIntegrations: RelatedIntegrationArray | undefined
|
||||
): ListItems[] => {
|
||||
if (relatedIntegrations == null || relatedIntegrations.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: label,
|
||||
description: <RelatedIntegrationsDescription integrations={relatedIntegrations} />,
|
||||
},
|
||||
];
|
||||
};
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { IntegrationDescription } from '../integrations_description';
|
||||
import { getInstalledRelatedIntegrations } from '../utils';
|
||||
import { useInstalledIntegrations } from '../use_installed_integrations';
|
||||
import type { RelatedIntegrationArray } from '../../../../../../common/detection_engine/schemas/common';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
export interface IntegrationsPopoverProps {
|
||||
integrations: RelatedIntegrationArray;
|
||||
}
|
||||
|
||||
const IntegrationsPopoverWrapper = styled(EuiFlexGroup)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const PopoverTitleWrapper = styled(EuiPopoverTitle)`
|
||||
max-width: 390px;
|
||||
`;
|
||||
|
||||
const PopoverContentWrapper = styled('div')`
|
||||
max-height: 400px;
|
||||
max-width: 390px;
|
||||
overflow: auto;
|
||||
line-height: ${({ theme }) => theme.eui.euiLineHeight};
|
||||
`;
|
||||
|
||||
const IntegrationListItem = styled('li')`
|
||||
list-style-type: disc;
|
||||
margin-left: 25px;
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* 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: allInstalledIntegrations } = useInstalledIntegrations({ packages: [] });
|
||||
|
||||
const integrationDetails = getInstalledRelatedIntegrations(
|
||||
integrations,
|
||||
allInstalledIntegrations
|
||||
);
|
||||
|
||||
const totalRelatedIntegrationsInstalled = integrationDetails.filter((i) => i.is_enabled).length;
|
||||
const badgeTitle =
|
||||
allInstalledIntegrations != null
|
||||
? `${totalRelatedIntegrationsInstalled}/${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={'package'}
|
||||
color="hollow"
|
||||
data-test-subj={'IntegrationsDisplayPopoverButton'}
|
||||
onClick={() => setPopoverOpen(!isPopoverOpen)}
|
||||
onClickAriaLabel={badgeTitle}
|
||||
>
|
||||
{badgeTitle}
|
||||
</EuiBadge>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setPopoverOpen(!isPopoverOpen)}
|
||||
repositionOnScroll
|
||||
>
|
||||
<PopoverTitleWrapper data-test-subj={'IntegrationsPopoverTitle'}>
|
||||
{i18n.INTEGRATIONS_POPOVER_TITLE(integrations.length)}
|
||||
</PopoverTitleWrapper>
|
||||
<PopoverContentWrapper data-test-subj={'IntegrationsPopoverContentWrapper'}>
|
||||
<EuiText size={'s'}>{i18n.INTEGRATIONS_POPOVER_DESCRIPTION(integrations.length)}</EuiText>
|
||||
<EuiSpacer size={'s'} />
|
||||
<ul>
|
||||
{integrationDetails.map((integration, index) => (
|
||||
<IntegrationListItem key={`${integration.package_name}-${index}`}>
|
||||
<IntegrationDescription integration={integration} />
|
||||
</IntegrationListItem>
|
||||
))}
|
||||
</ul>
|
||||
</PopoverContentWrapper>
|
||||
</EuiPopover>
|
||||
</IntegrationsPopoverWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoizedIntegrationsPopover = React.memo(IntegrationsPopoverComponent);
|
||||
MemoizedIntegrationsPopover.displayName = 'IntegrationsPopover';
|
||||
|
||||
export const IntegrationsPopover =
|
||||
MemoizedIntegrationsPopover as typeof IntegrationsPopoverComponent;
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 { IntegrationDetails } from './utils';
|
||||
import {
|
||||
InstalledIntegrationArray,
|
||||
RelatedIntegrationArray,
|
||||
} from '../../../../../common/detection_engine/schemas/common';
|
||||
|
||||
export const relatedIntegrations: RelatedIntegrationArray = [
|
||||
{
|
||||
package: 'system',
|
||||
version: '1.6.4',
|
||||
},
|
||||
{
|
||||
package: 'aws',
|
||||
integration: 'cloudtrail',
|
||||
version: '~1.11.0',
|
||||
},
|
||||
];
|
||||
|
||||
export const installedIntegrationsBase: InstalledIntegrationArray = [
|
||||
{ package_name: 'system', package_title: 'System', package_version: '1.6.4', is_enabled: true },
|
||||
];
|
||||
|
||||
export const installedIntegrationsAWSCloudwatch: InstalledIntegrationArray = [
|
||||
{
|
||||
package_name: 'aws',
|
||||
package_title: 'AWS',
|
||||
package_version: '1.11.0',
|
||||
integration_name: 'billing',
|
||||
integration_title: 'AWS Billing Metrics',
|
||||
is_enabled: false,
|
||||
},
|
||||
{
|
||||
package_name: 'aws',
|
||||
package_title: 'AWS',
|
||||
package_version: '1.11.0',
|
||||
integration_name: 'cloudtrail',
|
||||
integration_title: 'AWS Cloudtrail Logs',
|
||||
is_enabled: false,
|
||||
},
|
||||
{
|
||||
package_name: 'aws',
|
||||
package_title: 'AWS',
|
||||
package_version: '1.11.0',
|
||||
integration_name: 'cloudwatch',
|
||||
integration_title: 'AWS CloudWatch',
|
||||
is_enabled: true,
|
||||
},
|
||||
{ package_name: 'system', package_title: 'System', package_version: '1.6.4', is_enabled: true },
|
||||
{
|
||||
package_name: 'atlassian_bitbucket',
|
||||
package_title: 'Atlassian Bitbucket',
|
||||
package_version: '1.0.1',
|
||||
integration_name: 'audit',
|
||||
integration_title: 'Audit Logs',
|
||||
is_enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const integrationDetailsUninstalled: IntegrationDetails = {
|
||||
package_name: 'test',
|
||||
package_title: 'Test',
|
||||
package_version: '1.2.3',
|
||||
integration_name: 'integration',
|
||||
integration_title: 'Integration',
|
||||
is_enabled: false,
|
||||
is_installed: false,
|
||||
target_version: '1.2.3',
|
||||
version_satisfied: false,
|
||||
};
|
||||
|
||||
export const integrationDetailsInstalled: IntegrationDetails = {
|
||||
package_name: 'test',
|
||||
package_title: 'Test',
|
||||
package_version: '1.2.3',
|
||||
integration_name: 'integration',
|
||||
integration_title: 'Integration',
|
||||
is_enabled: false,
|
||||
is_installed: true,
|
||||
target_version: '1.2.3',
|
||||
version_satisfied: true,
|
||||
};
|
||||
|
||||
export const integrationDetailsEnabled: IntegrationDetails = {
|
||||
package_name: 'test',
|
||||
package_title: 'Test',
|
||||
package_version: '1.1.3',
|
||||
integration_name: 'integration',
|
||||
integration_title: 'Integration',
|
||||
is_enabled: true,
|
||||
is_installed: true,
|
||||
target_version: '1.3.3',
|
||||
version_satisfied: false,
|
||||
};
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const INTEGRATIONS_INSTALLED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.relatedIntegrations.installedTitle',
|
||||
{
|
||||
defaultMessage: 'Installed',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_INSTALLED_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.relatedIntegrations.installedTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Integration is installed. Configure an integration policy and ensure Elastic Agents are assigned this policy to ingest compatible events.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_UNINSTALLED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.relatedIntegrations.uninstalledTitle',
|
||||
{
|
||||
defaultMessage: 'Not installed',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_UNINSTALLED_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.relatedIntegrations.uninstalledTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Integration is not installed. Follow the integration link to install and configure the integration.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_ENABLED = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.relatedIntegrations.enabledTitle',
|
||||
{
|
||||
defaultMessage: 'Installed: enabled',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_ENABLED_TOOLTIP = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.relatedIntegrations.enabledTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Integration is installed and an integration policy with the required configuration exists. Ensure Elastic Agents are assigned this policy to ingest compatible events.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_BADGE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.relatedIntegrations.badgeTitle',
|
||||
{
|
||||
defaultMessage: 'integrations',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_POPOVER_TITLE = (integrationsCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.detectionEngine.relatedIntegrations.popoverTitle', {
|
||||
values: { integrationsCount },
|
||||
defaultMessage:
|
||||
'[{integrationsCount}] Related {integrationsCount, plural, =1 {integration} other {integrations}} available',
|
||||
});
|
||||
|
||||
export const INTEGRATIONS_POPOVER_DESCRIPTION = (integrationsCount: number) =>
|
||||
i18n.translate('xpack.securitySolution.detectionEngine.relatedIntegrations.popoverDescription', {
|
||||
values: { integrationsCount },
|
||||
defaultMessage:
|
||||
'Install and configure {integrationsCount, plural, =1 {the below integration} other {one or more of the below integrations}} to ingest the necessary data for this detection rule:',
|
||||
});
|
||||
|
||||
export const INTEGRATIONS_INSTALLED_VERSION_TOOLTIP = (
|
||||
installedVersion: string,
|
||||
targetVersion: string
|
||||
) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.relatedIntegrations.popoverDescriptionInstalledVersionTooltip',
|
||||
{
|
||||
values: { installedVersion, targetVersion },
|
||||
defaultMessage:
|
||||
'Version mismatch -- please resolve! Installed version `{installedVersion}` when target version `{targetVersion}`',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_FETCH_FAILURE = i18n.translate(
|
||||
'xpack.securitySolution.containers.detectionEngine.relatedIntegrations.fetchFailDescription',
|
||||
{
|
||||
defaultMessage: 'Failed to fetch installed integrations',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('../../../containers/detection_engine/rules/api');
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { renderHook, cleanup } from '@testing-library/react-hooks';
|
||||
|
||||
import { useInstalledIntegrations } from './use_installed_integrations';
|
||||
|
||||
import * as api from '../../../containers/detection_engine/rules/api';
|
||||
import { useToasts } from '../../../../common/lib/kibana';
|
||||
|
||||
describe('useInstalledIntegrations', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const createReactQueryWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Turn retries off, otherwise we won't be able to test errors
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper: React.FC = ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
return wrapper;
|
||||
};
|
||||
|
||||
const render = () =>
|
||||
renderHook(
|
||||
() =>
|
||||
useInstalledIntegrations({
|
||||
packages: [],
|
||||
}),
|
||||
{
|
||||
wrapper: createReactQueryWrapper(),
|
||||
}
|
||||
);
|
||||
|
||||
it('calls the API via fetchInstalledIntegrations', async () => {
|
||||
const fetchInstalledIntegrations = jest.spyOn(api, 'fetchInstalledIntegrations');
|
||||
|
||||
const { waitForNextUpdate } = render();
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(fetchInstalledIntegrations).toHaveBeenCalledTimes(1);
|
||||
expect(fetchInstalledIntegrations).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ packages: [] })
|
||||
);
|
||||
});
|
||||
|
||||
it('fetches data from the API', async () => {
|
||||
const { result, waitForNextUpdate } = render();
|
||||
|
||||
// It starts from a loading state
|
||||
expect(result.current.isLoading).toEqual(true);
|
||||
expect(result.current.isSuccess).toEqual(false);
|
||||
expect(result.current.isError).toEqual(false);
|
||||
|
||||
// When fetchRuleExecutionEvents returns
|
||||
await waitForNextUpdate();
|
||||
|
||||
// It switches to a success state
|
||||
expect(result.current.isLoading).toEqual(false);
|
||||
expect(result.current.isSuccess).toEqual(true);
|
||||
expect(result.current.isError).toEqual(false);
|
||||
expect(result.current.data).toEqual([
|
||||
{
|
||||
integration_name: 'audit',
|
||||
integration_title: 'Audit Logs',
|
||||
is_enabled: true,
|
||||
package_name: 'atlassian_bitbucket',
|
||||
package_title: 'Atlassian Bitbucket',
|
||||
package_version: '1.0.1',
|
||||
},
|
||||
{
|
||||
is_enabled: true,
|
||||
package_name: 'system',
|
||||
package_title: 'System',
|
||||
package_version: '1.6.4',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// Skipping until we re-enable errors
|
||||
it.skip('handles exceptions from the API', async () => {
|
||||
const exception = new Error('Boom!');
|
||||
jest.spyOn(api, 'fetchInstalledIntegrations').mockRejectedValue(exception);
|
||||
|
||||
const { result, waitForNextUpdate } = render();
|
||||
|
||||
// It starts from a loading state
|
||||
expect(result.current.isLoading).toEqual(true);
|
||||
expect(result.current.isSuccess).toEqual(false);
|
||||
expect(result.current.isError).toEqual(false);
|
||||
|
||||
// When fetchRuleExecutionEvents throws
|
||||
await waitForNextUpdate();
|
||||
|
||||
// It switches to an error state
|
||||
expect(result.current.isLoading).toEqual(false);
|
||||
expect(result.current.isSuccess).toEqual(false);
|
||||
expect(result.current.isError).toEqual(true);
|
||||
expect(result.current.error).toEqual(exception);
|
||||
|
||||
// And shows a toast with the caught exception
|
||||
expect(useToasts().addError).toHaveBeenCalledTimes(1);
|
||||
expect(useToasts().addError).toHaveBeenCalledWith(exception, {
|
||||
title: 'Failed to fetch installed integrations',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,19 +6,19 @@
|
|||
*/
|
||||
|
||||
import { useQuery } from 'react-query';
|
||||
import { GetInstalledIntegrationsResponse } from '../../../../../common/detection_engine/schemas/response/get_installed_integrations_response_schema';
|
||||
import { fetchInstalledIntegrations } from './api';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import * as i18n from './translations';
|
||||
import { InstalledIntegrationArray } from '../../../../../common/detection_engine/schemas/common';
|
||||
import { fetchInstalledIntegrations } from '../../../containers/detection_engine/rules/api';
|
||||
// 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();
|
||||
// const { addError } = useAppToasts();
|
||||
|
||||
return useQuery<GetInstalledIntegrationsResponse>(
|
||||
return useQuery<InstalledIntegrationArray>(
|
||||
[
|
||||
'installedIntegrations',
|
||||
{
|
||||
|
@ -26,15 +26,17 @@ export const useInstalledIntegrations = ({ packages }: UseInstalledIntegrationsA
|
|||
},
|
||||
],
|
||||
async ({ signal }) => {
|
||||
return fetchInstalledIntegrations({
|
||||
const integrations = await fetchInstalledIntegrations({
|
||||
packages,
|
||||
signal,
|
||||
});
|
||||
return integrations.installed_integrations ?? [];
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
onError: (e) => {
|
||||
addError(e, { title: i18n.INSTALLED_INTEGRATIONS_FETCH_FAILURE });
|
||||
// Suppressing for now to prevent excessive errors when fleet isn't configured
|
||||
// addError(e, { title: i18n.INTEGRATIONS_FETCH_FAILURE });
|
||||
},
|
||||
}
|
||||
);
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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 {
|
||||
integrationDetailsEnabled,
|
||||
integrationDetailsInstalled,
|
||||
integrationDetailsUninstalled,
|
||||
} from './mock';
|
||||
import { render } from '@testing-library/react';
|
||||
import { getInstalledRelatedIntegrations, getIntegrationLink } from './utils';
|
||||
|
||||
describe('Related Integrations Utilities', () => {
|
||||
describe('#getIntegrationLink', () => {
|
||||
describe('it returns a correctly formatted integrations link', () => {
|
||||
test('given an uninstalled integrationDetails', () => {
|
||||
const link = getIntegrationLink(integrationDetailsUninstalled, 'http://localhost');
|
||||
const { container } = render(link);
|
||||
|
||||
expect(container.firstChild).toHaveProperty(
|
||||
'href',
|
||||
'http://localhost/app/integrations/detail/test-1.2.3/overview?integration=integration'
|
||||
);
|
||||
});
|
||||
|
||||
test('given an installed integrationDetails', () => {
|
||||
const link = getIntegrationLink(integrationDetailsInstalled, 'http://localhost');
|
||||
const { container } = render(link);
|
||||
|
||||
expect(container.firstChild).toHaveProperty(
|
||||
'href',
|
||||
'http://localhost/app/integrations/detail/test-1.2.3/overview?integration=integration'
|
||||
);
|
||||
});
|
||||
|
||||
test('given an enabled integrationDetails with an unsatisfied version', () => {
|
||||
const link = getIntegrationLink(integrationDetailsEnabled, 'http://localhost');
|
||||
const { container } = render(link);
|
||||
|
||||
expect(container.firstChild).toHaveProperty(
|
||||
'href',
|
||||
'http://localhost/app/integrations/detail/test-1.3.3/overview?integration=integration'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getInstalledRelatedIntegrations', () => {
|
||||
test('it returns a the correct integrationDetails', () => {
|
||||
const integrationDetails = getInstalledRelatedIntegrations([], []);
|
||||
|
||||
expect(integrationDetails.length).toEqual(0);
|
||||
});
|
||||
|
||||
describe('version is correctly computed', () => {
|
||||
test('Unknown integration that does not exist', () => {
|
||||
const integrationDetails = getInstalledRelatedIntegrations(
|
||||
[
|
||||
{
|
||||
package: 'foo1',
|
||||
version: '~1.2.3',
|
||||
},
|
||||
{
|
||||
package: 'foo2',
|
||||
version: '^1.2.3',
|
||||
},
|
||||
{
|
||||
package: 'foo3',
|
||||
version: '1.2.x',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
expect(integrationDetails[0].target_version).toEqual('1.2.3');
|
||||
expect(integrationDetails[1].target_version).toEqual('1.2.3');
|
||||
expect(integrationDetails[2].target_version).toEqual('1.2.0');
|
||||
});
|
||||
|
||||
test('Integration that is not installed', () => {
|
||||
const integrationDetails = getInstalledRelatedIntegrations(
|
||||
[
|
||||
{
|
||||
package: 'aws',
|
||||
integration: 'route53',
|
||||
version: '~1.2.3',
|
||||
},
|
||||
{
|
||||
package: 'system',
|
||||
version: '^1.2.3',
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
expect(integrationDetails[0].target_version).toEqual('1.2.3');
|
||||
expect(integrationDetails[1].target_version).toEqual('1.2.3');
|
||||
});
|
||||
|
||||
test('Integration that is installed, and its version matches required version', () => {
|
||||
const integrationDetails = getInstalledRelatedIntegrations(
|
||||
[
|
||||
{
|
||||
package: 'aws',
|
||||
integration: 'route53',
|
||||
version: '^1.2.3',
|
||||
},
|
||||
{
|
||||
package: 'system',
|
||||
version: '~1.2.3',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
package_name: 'aws',
|
||||
package_title: 'AWS',
|
||||
package_version: '1.3.0',
|
||||
integration_name: 'route53',
|
||||
integration_title: 'AWS Route 53',
|
||||
is_enabled: false,
|
||||
},
|
||||
{
|
||||
package_name: 'system',
|
||||
package_title: 'System',
|
||||
package_version: '1.2.5',
|
||||
is_enabled: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Since version is satisfied, we check `package_version`
|
||||
expect(integrationDetails[0].version_satisfied).toEqual(true);
|
||||
expect(integrationDetails[0].package_version).toEqual('1.3.0');
|
||||
expect(integrationDetails[1].version_satisfied).toEqual(true);
|
||||
expect(integrationDetails[1].package_version).toEqual('1.2.5');
|
||||
});
|
||||
|
||||
test('Integration that is installed, and its version is less than required version', () => {
|
||||
const integrationDetails = getInstalledRelatedIntegrations(
|
||||
[
|
||||
{
|
||||
package: 'aws',
|
||||
integration: 'route53',
|
||||
version: '~1.2.3',
|
||||
},
|
||||
{
|
||||
package: 'system',
|
||||
version: '^1.2.3',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
package_name: 'aws',
|
||||
package_title: 'AWS',
|
||||
package_version: '1.2.0',
|
||||
integration_name: 'route53',
|
||||
integration_title: 'AWS Route 53',
|
||||
is_enabled: false,
|
||||
},
|
||||
{
|
||||
package_name: 'system',
|
||||
package_title: 'System',
|
||||
package_version: '1.2.2',
|
||||
is_enabled: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
expect(integrationDetails[0].target_version).toEqual('1.2.3');
|
||||
expect(integrationDetails[1].target_version).toEqual('1.2.3');
|
||||
});
|
||||
|
||||
test('Integration that is installed, and its version is greater than required version', () => {
|
||||
const integrationDetails = getInstalledRelatedIntegrations(
|
||||
[
|
||||
{
|
||||
package: 'aws',
|
||||
integration: 'route53',
|
||||
version: '^1.2.3',
|
||||
},
|
||||
{
|
||||
package: 'system',
|
||||
version: '~1.2.3',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
package_name: 'aws',
|
||||
package_title: 'AWS',
|
||||
package_version: '2.0.1',
|
||||
integration_name: 'route53',
|
||||
integration_title: 'AWS Route 53',
|
||||
is_enabled: false,
|
||||
},
|
||||
{
|
||||
package_name: 'system',
|
||||
package_title: 'System',
|
||||
package_version: '1.3.0',
|
||||
is_enabled: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
expect(integrationDetails[0].target_version).toEqual('1.2.3');
|
||||
expect(integrationDetails[1].target_version).toEqual('1.2.3');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 {
|
||||
InstalledIntegration,
|
||||
InstalledIntegrationArray,
|
||||
RelatedIntegration,
|
||||
RelatedIntegrationArray,
|
||||
} from '../../../../../common/detection_engine/schemas/common';
|
||||
|
||||
/**
|
||||
* Returns an `EuiLink` that will link to a given package/integration/version page within fleet
|
||||
*
|
||||
* @param integration IntegrationDetails describing a package/integrations installed state
|
||||
* @param basePath kbn basepath for composing the fleet URL
|
||||
*/
|
||||
export const getIntegrationLink = (integration: IntegrationDetails, basePath: string) => {
|
||||
const packageName = integration.package_name;
|
||||
const integrationName = integration.integration_name;
|
||||
const integrationTitle = integration.integration_title ?? integration.package_title;
|
||||
const version = integration.version_satisfied
|
||||
? integration.package_version
|
||||
: integration.target_version;
|
||||
|
||||
const integrationURL =
|
||||
version !== ''
|
||||
? `${basePath}/app/integrations/detail/${packageName}-${version}/overview${
|
||||
integrationName ? `?integration=${integrationName}` : ''
|
||||
}`
|
||||
: `${basePath}/app/integrations/detail/${packageName}`;
|
||||
return (
|
||||
<EuiLink href={integrationURL} target="_blank" data-test-subj={'integrationLink'}>
|
||||
{integrationTitle}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IntegrationDetails extends InstalledIntegration {
|
||||
target_version: string;
|
||||
version_satisfied: boolean;
|
||||
is_installed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of integrations and an array of installed integrations this will return an
|
||||
* array of integrations augmented with install details like targetVersion, and `version_satisfied`
|
||||
* has
|
||||
* @param integrations
|
||||
* @param installedIntegrations
|
||||
*/
|
||||
export const getInstalledRelatedIntegrations = (
|
||||
integrations: RelatedIntegrationArray,
|
||||
installedIntegrations: InstalledIntegrationArray | undefined
|
||||
): IntegrationDetails[] => {
|
||||
const integrationDetails: IntegrationDetails[] = [];
|
||||
|
||||
integrations.forEach((i: RelatedIntegration) => {
|
||||
const match = installedIntegrations?.find(
|
||||
(installed) =>
|
||||
installed.package_name === i.package && installed?.integration_name === 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.package_version, i.version);
|
||||
const packageVersion = versionSatisfied
|
||||
? i.version
|
||||
: semver.valid(semver.coerce(i.version)) ?? '';
|
||||
integrationDetails.push({
|
||||
...match,
|
||||
target_version: packageVersion,
|
||||
version_satisfied: versionSatisfied,
|
||||
is_installed: true,
|
||||
});
|
||||
} else {
|
||||
const packageVersion = semver.valid(semver.coerce(i.version)) ?? '';
|
||||
// TODO: Add `title` to RelatedIntegration (or fetch from Fleet API) so we can accurately display the integration pretty name
|
||||
const integrationTitle =
|
||||
i.integration != null ? `${capitalize(i.package)} ${capitalize(i.integration)}` : undefined;
|
||||
integrationDetails.push({
|
||||
package_name: i.package,
|
||||
package_title: capitalize(i.package),
|
||||
package_version: packageVersion,
|
||||
integration_name: i.integration,
|
||||
integration_title: integrationTitle,
|
||||
target_version: packageVersion,
|
||||
version_satisfied: false,
|
||||
is_enabled: false,
|
||||
is_installed: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return integrationDetails.sort((a, b) => {
|
||||
if (a.integration_title != null && b.integration_title != null) {
|
||||
return a.integration_title.localeCompare(b.integration_title);
|
||||
} else if (a.integration_title != null) {
|
||||
return a.integration_title.localeCompare(b.package_title);
|
||||
} else if (b.integration_title != null) {
|
||||
return a.package_title.localeCompare(b.integration_title);
|
||||
} else {
|
||||
return a.package_title.localeCompare(b.package_title);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import {
|
||||
GetAggregateRuleExecutionEventsResponse,
|
||||
GetInstalledIntegrationsResponse,
|
||||
RulesSchema,
|
||||
} from '../../../../../../common/detection_engine/schemas/response';
|
||||
|
||||
|
@ -104,3 +105,30 @@ export const fetchRuleExecutionEvents = async ({
|
|||
|
||||
export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<string[]> =>
|
||||
Promise.resolve(['elastic', 'love', 'quality', 'code']);
|
||||
|
||||
export const fetchInstalledIntegrations = async ({
|
||||
packages,
|
||||
signal,
|
||||
}: {
|
||||
packages?: string[];
|
||||
signal?: AbortSignal;
|
||||
}): Promise<GetInstalledIntegrationsResponse> => {
|
||||
return Promise.resolve({
|
||||
installed_integrations: [
|
||||
{
|
||||
package_name: 'atlassian_bitbucket',
|
||||
package_title: 'Atlassian Bitbucket',
|
||||
package_version: '1.0.1',
|
||||
integration_name: 'audit',
|
||||
integration_title: 'Audit Logs',
|
||||
is_enabled: true,
|
||||
},
|
||||
{
|
||||
package_name: 'system',
|
||||
package_title: 'System',
|
||||
package_version: '1.6.4',
|
||||
is_enabled: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
|
|
@ -123,10 +123,3 @@ 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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -15,11 +15,12 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { IntegrationsPopover } from '../../../../../common/components/integrations_popover';
|
||||
import { IntegrationsPopover } from '../../../../components/rules/related_integrations/integrations_popover';
|
||||
import {
|
||||
APP_UI_ID,
|
||||
DEFAULT_RELATIVE_DATE_THRESHOLD,
|
||||
SecurityPageName,
|
||||
SHOW_RELATED_INTEGRATIONS_SETTING,
|
||||
} from '../../../../../../common/constants';
|
||||
import { isMlRule } from '../../../../../../common/machine_learning/helpers';
|
||||
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
|
||||
|
@ -28,7 +29,7 @@ import { LinkAnchor } from '../../../../../common/components/links';
|
|||
import { useFormatUrl } from '../../../../../common/components/link_to';
|
||||
import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine';
|
||||
import { PopoverItems } from '../../../../../common/components/popover_items';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { useKibana, useUiSetting$ } from '../../../../../common/lib/kibana';
|
||||
import { canEditRuleWithActions, getToolTipContent } from '../../../../../common/utils/privileges';
|
||||
import { RuleSwitch } from '../../../../components/rules/rule_switch';
|
||||
import { SeverityBadge } from '../../../../components/rules/severity_badge';
|
||||
|
@ -163,7 +164,7 @@ const INTEGRATIONS_COLUMN: TableColumn = {
|
|||
name: null,
|
||||
align: 'center',
|
||||
render: (integrations: Rule['related_integrations']) => {
|
||||
if (integrations?.length === 0) {
|
||||
if (integrations == null || integrations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -201,11 +202,12 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[]
|
|||
const enabledColumn = useEnabledColumn({ hasPermissions });
|
||||
const ruleNameColumn = useRuleNameColumn();
|
||||
const { isInMemorySorting } = useRulesTableContext().state;
|
||||
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
ruleNameColumn,
|
||||
INTEGRATIONS_COLUMN,
|
||||
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
|
||||
TAGS_COLUMN,
|
||||
{
|
||||
field: 'risk_score',
|
||||
|
@ -294,7 +296,14 @@ export const useRulesColumns = ({ hasPermissions }: ColumnsProps): TableColumn[]
|
|||
enabledColumn,
|
||||
...(hasPermissions ? [actionsColumn] : []),
|
||||
],
|
||||
[actionsColumn, enabledColumn, hasPermissions, isInMemorySorting, ruleNameColumn]
|
||||
[
|
||||
actionsColumn,
|
||||
enabledColumn,
|
||||
hasPermissions,
|
||||
isInMemorySorting,
|
||||
ruleNameColumn,
|
||||
showRelatedIntegrations,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -304,6 +313,7 @@ export const useMonitoringColumns = ({ hasPermissions }: ColumnsProps): TableCol
|
|||
const enabledColumn = useEnabledColumn({ hasPermissions });
|
||||
const ruleNameColumn = useRuleNameColumn();
|
||||
const { isInMemorySorting } = useRulesTableContext().state;
|
||||
const [showRelatedIntegrations] = useUiSetting$<boolean>(SHOW_RELATED_INTEGRATIONS_SETTING);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
|
@ -311,7 +321,7 @@ export const useMonitoringColumns = ({ hasPermissions }: ColumnsProps): TableCol
|
|||
...ruleNameColumn,
|
||||
width: '28%',
|
||||
},
|
||||
INTEGRATIONS_COLUMN,
|
||||
...(showRelatedIntegrations ? [INTEGRATIONS_COLUMN] : []),
|
||||
TAGS_COLUMN,
|
||||
{
|
||||
field: 'execution_summary.last_execution.metrics.total_indexing_duration_ms',
|
||||
|
@ -426,6 +436,7 @@ export const useMonitoringColumns = ({ hasPermissions }: ColumnsProps): TableCol
|
|||
hasPermissions,
|
||||
isInMemorySorting,
|
||||
ruleNameColumn,
|
||||
showRelatedIntegrations,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1085,53 +1085,3 @@ export const RULES_BULK_EDIT_FAILURE_DESCRIPTION = (rulesCount: number) =>
|
|||
defaultMessage: '{rulesCount, plural, =1 {# rule} other {# rules}} failed to update.',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_BADGE = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.integrations.badgeTitle',
|
||||
{
|
||||
defaultMessage: 'integrations',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_POPOVER_TITLE = (integrationsCount: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverTitle',
|
||||
{
|
||||
values: { integrationsCount },
|
||||
defaultMessage:
|
||||
'You have [{integrationsCount}] related {integrationsCount, plural, =1 {integration} other {integrations}} to your prebuilt rule',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_POPOVER_DESCRIPTION_INSTALLED = (installedCount: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverDescriptionInstalledTitle',
|
||||
{
|
||||
values: { installedCount },
|
||||
defaultMessage:
|
||||
'You have [{installedCount}] related {installedCount, plural, =1 {integration} other {integrations}} installed, click the link below to view the integration:',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_POPOVER_DESCRIPTION_UNINSTALLED = (uninstalledCount: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverDescriptionUninstalledTitle',
|
||||
{
|
||||
values: { uninstalledCount },
|
||||
defaultMessage:
|
||||
'You have [{uninstalledCount}] related {uninstalledCount, plural, =1 {integration} other {integrations}} uninstalled, click the link to add integration:',
|
||||
}
|
||||
);
|
||||
|
||||
export const INTEGRATIONS_INSTALLED_VERSION_TOOLTIP = (
|
||||
installedVersion: string,
|
||||
targetVersion: string
|
||||
) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.allRules.integrations.popoverDescriptionInstalledVersionTooltip',
|
||||
{
|
||||
values: { installedVersion, targetVersion },
|
||||
defaultMessage:
|
||||
'Version mismatch -- please resolve! Installed version `{installedVersion}` when target version `{targetVersion}`',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
NEWS_FEED_URL_SETTING,
|
||||
NEWS_FEED_URL_SETTING_DEFAULT,
|
||||
ENABLE_CCS_READ_WARNING_SETTING,
|
||||
SHOW_RELATED_INTEGRATIONS_SETTING,
|
||||
} from '../common/constants';
|
||||
import { ExperimentalFeatures } from '../common/experimental_features';
|
||||
|
||||
|
@ -186,7 +187,7 @@ export const initUiSettings = (
|
|||
'xpack.securitySolution.uiSettings.rulesTableRefreshDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'<p>Enables auto refresh on the all rules and monitoring tables, in milliseconds</p>',
|
||||
'<p>Enables auto refresh on the rules and monitoring tables, in milliseconds</p>',
|
||||
}
|
||||
),
|
||||
type: 'json',
|
||||
|
@ -250,6 +251,22 @@ export const initUiSettings = (
|
|||
requiresPageReload: false,
|
||||
schema: schema.boolean(),
|
||||
},
|
||||
[SHOW_RELATED_INTEGRATIONS_SETTING]: {
|
||||
name: i18n.translate('xpack.securitySolution.uiSettings.showRelatedIntegrationsLabel', {
|
||||
defaultMessage: 'Related integrations',
|
||||
}),
|
||||
value: true,
|
||||
description: i18n.translate(
|
||||
'xpack.securitySolution.uiSettings.showRelatedIntegrationsDescription',
|
||||
{
|
||||
defaultMessage: '<p>Shows related integrations on the rules and monitoring tables</p>',
|
||||
}
|
||||
),
|
||||
type: 'boolean',
|
||||
category: [APP_ID],
|
||||
requiresPageReload: true,
|
||||
schema: schema.boolean(),
|
||||
},
|
||||
};
|
||||
|
||||
uiSettings.register(orderSettings(securityUiSettings));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue