mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[GenAI] Add License/productType controls to the integration assistant (#187105)
## Summary Adds the licence and product type controls to the Integration assistant. ### UI - ESS Licence: The `Enterprise` license must be active to use the feature. It is checked inside the _integration_assistant_ plugin itself by default and the license paywall is displayed in case the license requirement is not met, no need to configure anything from outside for the license check. - Serverless Product type: The `Security complete` tier must be enabled to use the feature in security projects. The _integration_assistant_ plugin exposes a contract API to set up a custom upselling. The product type check is performed in the _security_solution_serverless_ plugin and the upselling component is passed to the _integration_assistant_ plugin to be rendered. ### API The `withAvailability` wrapper has been introduced to encapsulate this availability check on all the routes. The `isAvailable` flag is defined at a plugin level and passed to the router context. The flag is defaulted to `true` and can be set to `false` by not having the `Enterprise` license (ESS), or by calling the `setIsActive(false)` contract exposed (serverless). All API requests done while the license / product type requirements are not met will be responded with: ``` 404 Not Found: This API route is not available using your current license/tier. ``` ### Screenshots ESS:  Serverless:  --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
2daa13d458
commit
51c7afd701
49 changed files with 938 additions and 420 deletions
|
@ -72,6 +72,11 @@ export enum ProductFeatureSecurityKey {
|
|||
* enables Cloud Security Posture - CSPM, KSPM, CNVM
|
||||
*/
|
||||
cloudSecurityPosture = 'cloud_security_posture',
|
||||
|
||||
/**
|
||||
* enables the integration assistant
|
||||
*/
|
||||
integrationAssistant = 'integration_assistant',
|
||||
}
|
||||
|
||||
export enum ProductFeatureCasesKey {
|
||||
|
|
|
@ -122,4 +122,5 @@ export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeature
|
|||
[ProductFeatureSecurityKey.endpointAgentTamperProtection]: {},
|
||||
[ProductFeatureSecurityKey.externalRuleActions]: {},
|
||||
[ProductFeatureSecurityKey.cloudSecurityPosture]: {},
|
||||
[ProductFeatureSecurityKey.integrationAssistant]: {},
|
||||
};
|
||||
|
|
|
@ -18,7 +18,8 @@ export type UpsellingSectionId =
|
|||
| 'endpoint_protection_updates'
|
||||
| 'endpoint_agent_tamper_protection'
|
||||
| 'cloud_security_posture_integration_installation'
|
||||
| 'ruleDetailsEndpointExceptions';
|
||||
| 'ruleDetailsEndpointExceptions'
|
||||
| 'integration_assistant';
|
||||
|
||||
export type UpsellingMessageId =
|
||||
| 'investigation_guide'
|
||||
|
|
|
@ -46,7 +46,7 @@ export const DefaultLayout: React.FC<Props> = memo(
|
|||
},
|
||||
];
|
||||
|
||||
const CreateIntegrationCardButton = integrationAssistant?.CreateIntegrationCardButton;
|
||||
const { CreateIntegrationCardButton } = integrationAssistant?.components ?? {};
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { useStartServices, useBreadcrumbs } from '../../../../hooks';
|
||||
|
||||
|
@ -13,10 +13,7 @@ export const CreateIntegration = React.memo(() => {
|
|||
const { integrationAssistant } = useStartServices();
|
||||
useBreadcrumbs('integration_create');
|
||||
|
||||
const CreateIntegrationAssistant = useMemo(
|
||||
() => integrationAssistant?.CreateIntegration,
|
||||
[integrationAssistant]
|
||||
);
|
||||
const CreateIntegrationAssistant = integrationAssistant?.components.CreateIntegration;
|
||||
|
||||
return CreateIntegrationAssistant ? <CreateIntegrationAssistant /> : null;
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
],
|
||||
"requiredPlugins": [
|
||||
"kibanaReact",
|
||||
"licensing",
|
||||
"triggersActionsUi",
|
||||
"actions",
|
||||
"stackConnectors",
|
||||
|
|
|
@ -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 React, { type PropsWithChildren } from 'react';
|
||||
import { LicensePaywallCard } from './license_paywall_card';
|
||||
import { useAvailability } from '../../hooks/use_availability';
|
||||
|
||||
type AvailabilityWrapperProps = PropsWithChildren<{}>;
|
||||
export const AvailabilityWrapper = React.memo<AvailabilityWrapperProps>(({ children }) => {
|
||||
const { hasLicense, renderUpselling } = useAvailability();
|
||||
if (renderUpselling) {
|
||||
return <>{renderUpselling}</>;
|
||||
}
|
||||
if (!hasLicense) {
|
||||
return <LicensePaywallCard />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
});
|
||||
AvailabilityWrapper.displayName = 'AvailabilityWrapper';
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
export { AvailabilityWrapper } from './availability_wrapper';
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiCard,
|
||||
EuiIcon,
|
||||
EuiTextColor,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const LicensePaywallCard = React.memo(() => {
|
||||
const { getUrlForApp } = useKibana().services.application;
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCard
|
||||
data-test-subj={'LicensePaywallCard'}
|
||||
betaBadgeProps={{
|
||||
label: i18n.ENTERPRISE_LICENSE_LABEL,
|
||||
}}
|
||||
isDisabled={true}
|
||||
icon={<EuiIcon size="xl" type="lock" />}
|
||||
title={
|
||||
<h3>
|
||||
<strong>{i18n.ENTERPRISE_LICENSE_TITLE}</strong>
|
||||
</h3>
|
||||
}
|
||||
description={false}
|
||||
>
|
||||
<EuiFlexGroup className="lockedCardDescription" direction="column" justifyContent="center">
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText>
|
||||
<h4>
|
||||
<EuiTextColor color="subdued">{i18n.ENTERPRISE_LICENSE_UPGRADE_TITLE}</EuiTextColor>
|
||||
</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>{i18n.ENTERPRISE_LICENSE_UPGRADE_DESCRIPTION}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div>
|
||||
<EuiButton
|
||||
href={getUrlForApp('management', { path: 'stack/license_management' })}
|
||||
fill
|
||||
>
|
||||
{i18n.ENTERPRISE_LICENSE_UPGRADE_BUTTON}
|
||||
</EuiButton>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCard>
|
||||
</>
|
||||
);
|
||||
});
|
||||
LicensePaywallCard.displayName = 'LicensePaywallCard';
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 ENTERPRISE_LICENSE_LABEL = i18n.translate(
|
||||
'xpack.integrationAssistant.license.enterprise.label',
|
||||
{
|
||||
defaultMessage: 'Enterprise',
|
||||
}
|
||||
);
|
||||
|
||||
export const ENTERPRISE_LICENSE_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.license.enterprise.title',
|
||||
{
|
||||
defaultMessage: 'Enterprise License Required',
|
||||
}
|
||||
);
|
||||
export const ENTERPRISE_LICENSE_UPGRADE_TITLE = i18n.translate(
|
||||
'xpack.integrationAssistant.license.enterprise.upgradeTitle',
|
||||
{
|
||||
defaultMessage: 'Upgrade to Elastic Enterprise',
|
||||
}
|
||||
);
|
||||
export const ENTERPRISE_LICENSE_UPGRADE_DESCRIPTION = i18n.translate(
|
||||
'xpack.integrationAssistant.license.enterprise.upgradeDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'To turn on this feature, you must upgrade your license to Enterprise or start a free 30-day trial',
|
||||
}
|
||||
);
|
||||
export const ENTERPRISE_LICENSE_UPGRADE_BUTTON = i18n.translate(
|
||||
'xpack.integrationAssistant.license.enterprise.upgradeButton',
|
||||
{
|
||||
defaultMessage: 'Upgrade license',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
import type { LicenseType } from '@kbn/licensing-plugin/public';
|
||||
import { useKibana } from './use_kibana';
|
||||
import type { RenderUpselling } from '../../services';
|
||||
|
||||
const MinimumLicenseRequired: LicenseType = 'enterprise';
|
||||
|
||||
export const useAvailability = (): {
|
||||
hasLicense: boolean;
|
||||
renderUpselling: RenderUpselling | undefined;
|
||||
} => {
|
||||
const { licensing, renderUpselling$ } = useKibana().services;
|
||||
const licenseService = useObservable(licensing.license$);
|
||||
const renderUpselling = useObservable(renderUpselling$);
|
||||
const hasLicense = useMemo(
|
||||
() => licenseService?.hasAtLeast(MinimumLicenseRequired) ?? true,
|
||||
[licenseService]
|
||||
);
|
||||
return { hasLicense, renderUpselling };
|
||||
};
|
||||
|
||||
export const useIsAvailable = (): boolean => {
|
||||
const { hasLicense, renderUpselling } = useAvailability();
|
||||
return hasLicense && !renderUpselling;
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Switch } from 'react-router-dom';
|
||||
import { Redirect, Switch } from 'react-router-dom';
|
||||
import { Route } from '@kbn/shared-ux-router';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { Services } from '../../services';
|
||||
|
@ -15,6 +15,7 @@ import { CreateIntegrationUpload } from './create_integration_upload';
|
|||
import { CreateIntegrationAssistant } from './create_integration_assistant';
|
||||
import { Page, PagePath } from '../../common/constants';
|
||||
import { useRoutesAuthorization } from '../../common/hooks/use_authorization';
|
||||
import { useIsAvailable } from '../../common/hooks/use_availability';
|
||||
|
||||
interface CreateIntegrationProps {
|
||||
services: Services;
|
||||
|
@ -26,21 +27,23 @@ export const CreateIntegration = React.memo<CreateIntegrationProps>(({ services
|
|||
</TelemetryContextProvider>
|
||||
</KibanaContextProvider>
|
||||
));
|
||||
|
||||
CreateIntegration.displayName = 'CreateIntegration';
|
||||
|
||||
const CreateIntegrationRouter = React.memo(() => {
|
||||
const { canUseIntegrationAssistant, canUseIntegrationUpload } = useRoutesAuthorization();
|
||||
|
||||
const isAvailable = useIsAvailable();
|
||||
return (
|
||||
<Switch>
|
||||
{canUseIntegrationAssistant && (
|
||||
<Route path={PagePath[Page.assistant]} component={CreateIntegrationAssistant} />
|
||||
{isAvailable && canUseIntegrationAssistant && (
|
||||
<Route path={PagePath[Page.assistant]} exact component={CreateIntegrationAssistant} />
|
||||
)}
|
||||
{canUseIntegrationUpload && (
|
||||
<Route path={PagePath[Page.upload]} component={CreateIntegrationUpload} />
|
||||
{isAvailable && canUseIntegrationUpload && (
|
||||
<Route path={PagePath[Page.upload]} exact component={CreateIntegrationUpload} />
|
||||
)}
|
||||
<Route path={PagePath[Page.landing]} component={CreateIntegrationLanding} />
|
||||
|
||||
<Route path={PagePath[Page.landing]} exact component={CreateIntegrationLanding} />
|
||||
|
||||
<Route render={() => <Redirect to={PagePath[Page.landing]} />} />
|
||||
</Switch>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,100 +6,18 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCard,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { AssistantAvatar } from '@kbn/elastic-assistant';
|
||||
import { css } from '@emotion/react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useAuthorization } from '../../../common/hooks/use_authorization';
|
||||
import {
|
||||
AuthorizationWrapper,
|
||||
MissingPrivilegesTooltip,
|
||||
} from '../../../common/components/authorization';
|
||||
import { AuthorizationWrapper } from '../../../common/components/authorization';
|
||||
import { AvailabilityWrapper } from '../../../common/components/availability_wrapper';
|
||||
import { IntegrationImageHeader } from '../../../common/components/integration_image_header';
|
||||
import { ButtonsFooter } from '../../../common/components/buttons_footer';
|
||||
import { SectionWrapper } from '../../../common/components/section_wrapper';
|
||||
import { useNavigate, Page } from '../../../common/hooks/use_navigate';
|
||||
import { IntegrationAssistantCard } from './integration_assistant_card';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const useAssistantCardCss = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return css`
|
||||
/* compensate for EuiCard children margin-block-start */
|
||||
margin-block-start: calc(${euiTheme.size.s} * -2);
|
||||
`;
|
||||
};
|
||||
|
||||
const IntegrationAssistantCard = React.memo(() => {
|
||||
const { canExecuteConnectors } = useAuthorization();
|
||||
const navigate = useNavigate();
|
||||
const assistantCardCss = useAssistantCardCss();
|
||||
return (
|
||||
<EuiCard
|
||||
display="plain"
|
||||
hasBorder={true}
|
||||
paddingSize="l"
|
||||
title={''} // title shown inside the child component
|
||||
betaBadgeProps={{
|
||||
label: i18n.TECH_PREVIEW,
|
||||
tooltipContent: i18n.TECH_PREVIEW_TOOLTIP,
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
css={assistantCardCss}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantAvatar />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
alignItems="flexStart"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{i18n.ASSISTANT_TITLE}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued" textAlign="left">
|
||||
{i18n.ASSISTANT_DESCRIPTION}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{canExecuteConnectors ? (
|
||||
<EuiButton onClick={() => navigate(Page.assistant)}>{i18n.ASSISTANT_BUTTON}</EuiButton>
|
||||
) : (
|
||||
<MissingPrivilegesTooltip canExecuteConnectors>
|
||||
<EuiButton disabled>{i18n.ASSISTANT_BUTTON}</EuiButton>
|
||||
</MissingPrivilegesTooltip>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCard>
|
||||
);
|
||||
});
|
||||
IntegrationAssistantCard.displayName = 'IntegrationAssistantCard';
|
||||
|
||||
export const CreateIntegrationLanding = React.memo(() => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
|
@ -107,49 +25,51 @@ export const CreateIntegrationLanding = React.memo(() => {
|
|||
<IntegrationImageHeader />
|
||||
<KibanaPageTemplate.Section grow>
|
||||
<SectionWrapper title={i18n.LANDING_TITLE} subtitle={i18n.LANDING_DESCRIPTION}>
|
||||
<AuthorizationWrapper canCreateIntegrations>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="l"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="l" />
|
||||
<IntegrationAssistantCard />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="package" size="l" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationLanding.uploadPackageDescription"
|
||||
defaultMessage="If you have an existing integration package, {link}"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink onClick={() => navigate(Page.upload)}>
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationLanding.uploadPackageLink"
|
||||
defaultMessage="upload it as a .zip"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</AuthorizationWrapper>
|
||||
<AvailabilityWrapper>
|
||||
<AuthorizationWrapper canCreateIntegrations>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="l"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="l" />
|
||||
<IntegrationAssistantCard />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="package" size="l" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationLanding.uploadPackageDescription"
|
||||
defaultMessage="If you have an existing integration package, {link}"
|
||||
values={{
|
||||
link: (
|
||||
<EuiLink onClick={() => navigate(Page.upload)}>
|
||||
<FormattedMessage
|
||||
id="xpack.integrationAssistant.createIntegrationLanding.uploadPackageLink"
|
||||
defaultMessage="upload it as a .zip"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</AuthorizationWrapper>
|
||||
</AvailabilityWrapper>
|
||||
</SectionWrapper>
|
||||
</KibanaPageTemplate.Section>
|
||||
<ButtonsFooter />
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiCard,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { AssistantAvatar } from '@kbn/elastic-assistant';
|
||||
import { css } from '@emotion/react';
|
||||
import { useAuthorization } from '../../../common/hooks/use_authorization';
|
||||
import { MissingPrivilegesTooltip } from '../../../common/components/authorization';
|
||||
import { useNavigate, Page } from '../../../common/hooks/use_navigate';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const useAssistantCardCss = () => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return css`
|
||||
/* compensate for EuiCard children margin-block-start */
|
||||
margin-block-start: calc(${euiTheme.size.s} * -2);
|
||||
`;
|
||||
};
|
||||
|
||||
export const IntegrationAssistantCard = React.memo(() => {
|
||||
const { canExecuteConnectors } = useAuthorization();
|
||||
const navigate = useNavigate();
|
||||
const assistantCardCss = useAssistantCardCss();
|
||||
return (
|
||||
<EuiCard
|
||||
display="plain"
|
||||
hasBorder={true}
|
||||
paddingSize="l"
|
||||
title={''} // title shown inside the child component
|
||||
betaBadgeProps={{
|
||||
label: i18n.TECH_PREVIEW,
|
||||
tooltipContent: i18n.TECH_PREVIEW_TOOLTIP,
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="l"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
css={assistantCardCss}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantAvatar />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="s"
|
||||
alignItems="flexStart"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{i18n.ASSISTANT_TITLE}</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" color="subdued" textAlign="left">
|
||||
{i18n.ASSISTANT_DESCRIPTION}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{canExecuteConnectors ? (
|
||||
<EuiButton onClick={() => navigate(Page.assistant)}>{i18n.ASSISTANT_BUTTON}</EuiButton>
|
||||
) : (
|
||||
<MissingPrivilegesTooltip canExecuteConnectors>
|
||||
<EuiButton disabled>{i18n.ASSISTANT_BUTTON}</EuiButton>
|
||||
</MissingPrivilegesTooltip>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCard>
|
||||
);
|
||||
});
|
||||
IntegrationAssistantCard.displayName = 'IntegrationAssistantCard';
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { CoreStart, Plugin, CoreSetup } from '@kbn/core/public';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type {
|
||||
IntegrationAssistantPluginSetup,
|
||||
IntegrationAssistantPluginStart,
|
||||
|
@ -13,12 +14,13 @@ import type {
|
|||
} from './types';
|
||||
import { getCreateIntegrationLazy } from './components/create_integration';
|
||||
import { getCreateIntegrationCardButtonLazy } from './components/create_integration_card_button';
|
||||
import { Telemetry, type Services } from './services';
|
||||
import { Telemetry, type Services, type RenderUpselling } from './services';
|
||||
|
||||
export class IntegrationAssistantPlugin
|
||||
implements Plugin<IntegrationAssistantPluginSetup, IntegrationAssistantPluginStart>
|
||||
{
|
||||
private telemetry = new Telemetry();
|
||||
private renderUpselling$ = new BehaviorSubject<RenderUpselling | undefined>(undefined);
|
||||
|
||||
public setup(core: CoreSetup): IntegrationAssistantPluginSetup {
|
||||
this.telemetry.setup(core.analytics);
|
||||
|
@ -33,11 +35,17 @@ export class IntegrationAssistantPlugin
|
|||
...core,
|
||||
...dependencies,
|
||||
telemetry: this.telemetry.start(),
|
||||
renderUpselling$: this.renderUpselling$.asObservable(),
|
||||
};
|
||||
|
||||
return {
|
||||
CreateIntegration: getCreateIntegrationLazy(services),
|
||||
CreateIntegrationCardButton: getCreateIntegrationCardButtonLazy(),
|
||||
components: {
|
||||
CreateIntegration: getCreateIntegrationLazy(services),
|
||||
CreateIntegrationCardButton: getCreateIntegrationCardButtonLazy(),
|
||||
},
|
||||
renderUpselling: (renderUpselling) => {
|
||||
this.renderUpselling$.next(renderUpselling);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,12 +4,17 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { IntegrationAssistantPluginStartDependencies } from '../types';
|
||||
import type { TelemetryService } from './telemetry/service';
|
||||
|
||||
export { Telemetry } from './telemetry/service';
|
||||
|
||||
export type RenderUpselling = React.ReactNode;
|
||||
|
||||
export type Services = CoreStart &
|
||||
IntegrationAssistantPluginStartDependencies & { telemetry: TelemetryService };
|
||||
IntegrationAssistantPluginStartDependencies & {
|
||||
telemetry: TelemetryService;
|
||||
renderUpselling$: Observable<RenderUpselling | undefined>;
|
||||
};
|
||||
|
|
|
@ -4,25 +4,43 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import type {
|
||||
TriggersAndActionsUIPublicPluginSetup,
|
||||
TriggersAndActionsUIPublicPluginStart,
|
||||
} from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { CreateIntegrationComponent } from './components/create_integration/types';
|
||||
import type { CreateIntegrationCardButtonComponent } from './components/create_integration_card_button/types';
|
||||
import type { RenderUpselling } from './services';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface IntegrationAssistantPluginSetup {}
|
||||
|
||||
export interface IntegrationAssistantPluginStart {
|
||||
CreateIntegration: CreateIntegrationComponent;
|
||||
CreateIntegrationCardButton: CreateIntegrationCardButtonComponent;
|
||||
components: {
|
||||
/**
|
||||
* Component that allows the user to create an integration.
|
||||
*/
|
||||
CreateIntegration: CreateIntegrationComponent;
|
||||
/**
|
||||
* Component that links the user to the create integration component.
|
||||
*/
|
||||
CreateIntegrationCardButton: CreateIntegrationCardButtonComponent;
|
||||
};
|
||||
/**
|
||||
* Sets the upselling to be rendered in the UI.
|
||||
* If defined, the section will be displayed and it will prevent
|
||||
* the user from interacting with the rest of the UI.
|
||||
*/
|
||||
renderUpselling: (upselling: RenderUpselling | undefined) => void;
|
||||
}
|
||||
|
||||
export interface IntegrationAssistantPluginSetupDependencies {
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
}
|
||||
|
||||
export interface IntegrationAssistantPluginStartDependencies {
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
licensing: LicensingPluginStart;
|
||||
}
|
||||
|
|
|
@ -91,7 +91,6 @@ class MockServer {
|
|||
|
||||
public async inject(request: KibanaRequest, context: RequestHandlerContext = this.contextMock) {
|
||||
const validatedRequest = this.validateRequest(request);
|
||||
|
||||
const [rejection] = this.resultMock.badRequest.mock.calls;
|
||||
if (rejection) {
|
||||
throw new Error(`Request was rejected with message: '${rejection}'`);
|
||||
|
|
|
@ -72,6 +72,7 @@ const createRequestContextMock = (clients: MockClients = createMockClients()) =>
|
|||
];
|
||||
}
|
||||
),
|
||||
isAvailable: jest.fn((): boolean => true),
|
||||
logger: loggerMock.create(),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -40,6 +40,11 @@ const buildResponses = (method: Method, calls: MockCall[]): ResponseCall[] => {
|
|||
status: 400,
|
||||
body: call.body,
|
||||
}));
|
||||
case 'notFound':
|
||||
return calls.map(([call]) => ({
|
||||
status: 404,
|
||||
body: call.body,
|
||||
}));
|
||||
default:
|
||||
throw new Error(`Encountered unexpected call to response.${method}`);
|
||||
}
|
||||
|
|
|
@ -15,13 +15,18 @@ import type {
|
|||
} from '@kbn/core/server';
|
||||
import type { PluginStartContract as ActionsPluginsStart } from '@kbn/actions-plugin/server/plugin';
|
||||
import { registerRoutes } from './routes';
|
||||
import type { IntegrationAssistantPluginSetup, IntegrationAssistantPluginStart } from './types';
|
||||
import type {
|
||||
IntegrationAssistantPluginSetup,
|
||||
IntegrationAssistantPluginStart,
|
||||
IntegrationAssistantPluginStartDependencies,
|
||||
} from './types';
|
||||
|
||||
export type IntegrationAssistantRouteHandlerContext = CustomRequestHandlerContext<{
|
||||
integrationAssistant: {
|
||||
getStartServices: CoreSetup<{
|
||||
actions: ActionsPluginsStart;
|
||||
}>['getStartServices'];
|
||||
isAvailable: () => boolean;
|
||||
logger: Logger;
|
||||
};
|
||||
}>;
|
||||
|
@ -30,20 +35,24 @@ export class IntegrationAssistantPlugin
|
|||
implements Plugin<IntegrationAssistantPluginSetup, IntegrationAssistantPluginStart>
|
||||
{
|
||||
private readonly logger: Logger;
|
||||
private isAvailable: boolean;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.logger = initializerContext.logger.get();
|
||||
this.isAvailable = true;
|
||||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup<{
|
||||
actions: ActionsPluginsStart;
|
||||
}>
|
||||
) {
|
||||
): IntegrationAssistantPluginSetup {
|
||||
core.http.registerRouteHandlerContext<
|
||||
IntegrationAssistantRouteHandlerContext,
|
||||
'integrationAssistant'
|
||||
>('integrationAssistant', () => ({
|
||||
getStartServices: core.getStartServices,
|
||||
isAvailable: () => this.isAvailable,
|
||||
logger: this.logger,
|
||||
}));
|
||||
const router = core.http.createRouter<IntegrationAssistantRouteHandlerContext>();
|
||||
|
@ -51,11 +60,28 @@ export class IntegrationAssistantPlugin
|
|||
|
||||
registerRoutes(router);
|
||||
|
||||
return {};
|
||||
return {
|
||||
setIsAvailable: (isAvailable: boolean) => {
|
||||
if (!isAvailable) {
|
||||
this.isAvailable = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start(core: CoreStart) {
|
||||
public start(
|
||||
_: CoreStart,
|
||||
dependencies: IntegrationAssistantPluginStartDependencies
|
||||
): IntegrationAssistantPluginStart {
|
||||
this.logger.debug('integrationAssistant api: Started');
|
||||
const { licensing } = dependencies;
|
||||
|
||||
licensing.license$.subscribe((license) => {
|
||||
if (!license.hasAtLeast('enterprise')) {
|
||||
this.isAvailable = false;
|
||||
}
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -58,4 +58,15 @@ describe('registerIntegrationBuilderRoutes', () => {
|
|||
expect(response.body).toEqual({ test: 'test' });
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
||||
describe('when the integration assistant is not available', () => {
|
||||
beforeEach(() => {
|
||||
context.integrationAssistant.isAvailable.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('returns a 404', async () => {
|
||||
const response = await server.inject(req, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import { BuildIntegrationRequestBody, INTEGRATION_BUILDER_PATH } from '../../com
|
|||
import { buildPackage } from '../integration_builder';
|
||||
import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
|
||||
import { buildRouteValidationWithZod } from '../util/route_validation';
|
||||
import { withAvailability } from './with_availability';
|
||||
|
||||
export function registerIntegrationBuilderRoutes(
|
||||
router: IRouter<IntegrationAssistantRouteHandlerContext>
|
||||
|
@ -28,7 +29,7 @@ export function registerIntegrationBuilderRoutes(
|
|||
},
|
||||
},
|
||||
},
|
||||
async (_, request, response) => {
|
||||
withAvailability(async (_, request, response) => {
|
||||
const { integration } = request.body;
|
||||
try {
|
||||
const zippedIntegration = await buildPackage(integration);
|
||||
|
@ -40,6 +41,6 @@ export function registerIntegrationBuilderRoutes(
|
|||
} catch (e) {
|
||||
return response.customError({ statusCode: 500, body: e });
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -64,4 +64,15 @@ describe('registerCategorizationRoute', () => {
|
|||
const response = await server.inject(req, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
|
||||
describe('when the integration assistant is not available', () => {
|
||||
beforeEach(() => {
|
||||
context.integrationAssistant.isAvailable.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('returns a 404', async () => {
|
||||
const response = await server.inject(req, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ import { ROUTE_HANDLER_TIMEOUT } from '../constants';
|
|||
import { getCategorizationGraph } from '../graphs/categorization';
|
||||
import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
|
||||
import { buildRouteValidationWithZod } from '../util/route_validation';
|
||||
import { withAvailability } from './with_availability';
|
||||
|
||||
export function registerCategorizationRoutes(
|
||||
router: IRouter<IntegrationAssistantRouteHandlerContext>
|
||||
|
@ -43,49 +44,51 @@ export function registerCategorizationRoutes(
|
|||
},
|
||||
},
|
||||
},
|
||||
async (context, req, res): Promise<IKibanaResponse<CategorizationResponse>> => {
|
||||
const { packageName, dataStreamName, rawSamples, currentPipeline } = req.body;
|
||||
const services = await context.resolve(['core']);
|
||||
const { client } = services.core.elasticsearch;
|
||||
const { getStartServices, logger } = await context.integrationAssistant;
|
||||
const [, { actions: actionsPlugin }] = await getStartServices();
|
||||
withAvailability(
|
||||
async (context, req, res): Promise<IKibanaResponse<CategorizationResponse>> => {
|
||||
const { packageName, dataStreamName, rawSamples, currentPipeline } = req.body;
|
||||
const services = await context.resolve(['core']);
|
||||
const { client } = services.core.elasticsearch;
|
||||
const { getStartServices, logger } = await context.integrationAssistant;
|
||||
const [, { actions: actionsPlugin }] = await getStartServices();
|
||||
|
||||
try {
|
||||
const actionsClient = await actionsPlugin.getActionsClientWithRequest(req);
|
||||
const connector = req.body.connectorId
|
||||
? await actionsClient.get({ id: req.body.connectorId })
|
||||
: (await actionsClient.getAll()).filter(
|
||||
(connectorItem) => connectorItem.actionTypeId === '.bedrock'
|
||||
)[0];
|
||||
try {
|
||||
const actionsClient = await actionsPlugin.getActionsClientWithRequest(req);
|
||||
const connector = req.body.connectorId
|
||||
? await actionsClient.get({ id: req.body.connectorId })
|
||||
: (await actionsClient.getAll()).filter(
|
||||
(connectorItem) => connectorItem.actionTypeId === '.bedrock'
|
||||
)[0];
|
||||
|
||||
const abortSignal = getRequestAbortedSignal(req.events.aborted$);
|
||||
const isOpenAI = connector.actionTypeId === '.gen-ai';
|
||||
const llmClass = isOpenAI ? ActionsClientChatOpenAI : ActionsClientSimpleChatModel;
|
||||
const abortSignal = getRequestAbortedSignal(req.events.aborted$);
|
||||
const isOpenAI = connector.actionTypeId === '.gen-ai';
|
||||
const llmClass = isOpenAI ? ActionsClientChatOpenAI : ActionsClientSimpleChatModel;
|
||||
|
||||
const model = new llmClass({
|
||||
actions: actionsPlugin,
|
||||
connectorId: connector.id,
|
||||
request: req,
|
||||
logger,
|
||||
llmType: isOpenAI ? 'openai' : 'bedrock',
|
||||
model: connector.config?.defaultModel,
|
||||
temperature: 0.05,
|
||||
maxTokens: 4096,
|
||||
signal: abortSignal,
|
||||
streaming: false,
|
||||
});
|
||||
const model = new llmClass({
|
||||
actions: actionsPlugin,
|
||||
connectorId: connector.id,
|
||||
request: req,
|
||||
logger,
|
||||
llmType: isOpenAI ? 'openai' : 'bedrock',
|
||||
model: connector.config?.defaultModel,
|
||||
temperature: 0.05,
|
||||
maxTokens: 4096,
|
||||
signal: abortSignal,
|
||||
streaming: false,
|
||||
});
|
||||
|
||||
const graph = await getCategorizationGraph(client, model);
|
||||
const results = await graph.invoke({
|
||||
packageName,
|
||||
dataStreamName,
|
||||
rawSamples,
|
||||
currentPipeline,
|
||||
});
|
||||
return res.ok({ body: CategorizationResponse.parse(results) });
|
||||
} catch (e) {
|
||||
return res.badRequest({ body: e });
|
||||
const graph = await getCategorizationGraph(client, model);
|
||||
const results = await graph.invoke({
|
||||
packageName,
|
||||
dataStreamName,
|
||||
rawSamples,
|
||||
currentPipeline,
|
||||
});
|
||||
return res.ok({ body: CategorizationResponse.parse(results) });
|
||||
} catch (e) {
|
||||
return res.badRequest({ body: e });
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -69,4 +69,15 @@ describe('registerEcsRoute', () => {
|
|||
const response = await server.inject(req, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
|
||||
describe('when the integration assistant is not available', () => {
|
||||
beforeEach(() => {
|
||||
context.integrationAssistant.isAvailable.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('returns a 404', async () => {
|
||||
const response = await server.inject(req, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ import { ROUTE_HANDLER_TIMEOUT } from '../constants';
|
|||
import { getEcsGraph } from '../graphs/ecs';
|
||||
import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
|
||||
import { buildRouteValidationWithZod } from '../util/route_validation';
|
||||
import { withAvailability } from './with_availability';
|
||||
|
||||
export function registerEcsRoutes(router: IRouter<IntegrationAssistantRouteHandlerContext>) {
|
||||
router.versioned
|
||||
|
@ -37,9 +38,10 @@ export function registerEcsRoutes(router: IRouter<IntegrationAssistantRouteHandl
|
|||
},
|
||||
},
|
||||
},
|
||||
async (context, req, res): Promise<IKibanaResponse<EcsMappingResponse>> => {
|
||||
withAvailability(async (context, req, res): Promise<IKibanaResponse<EcsMappingResponse>> => {
|
||||
const { packageName, dataStreamName, rawSamples, mapping } = req.body;
|
||||
const { getStartServices, logger } = await context.integrationAssistant;
|
||||
|
||||
const [, { actions: actionsPlugin }] = await getStartServices();
|
||||
try {
|
||||
const actionsClient = await actionsPlugin.getActionsClientWithRequest(req);
|
||||
|
@ -86,6 +88,6 @@ export function registerEcsRoutes(router: IRouter<IntegrationAssistantRouteHandl
|
|||
} catch (e) {
|
||||
return res.badRequest({ body: e });
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -54,4 +54,15 @@ describe('registerPipelineRoutes', () => {
|
|||
});
|
||||
expect(response.status).toEqual(200);
|
||||
});
|
||||
|
||||
describe('when the integration assistant is not available', () => {
|
||||
beforeEach(() => {
|
||||
context.integrationAssistant.isAvailable.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('returns a 404', async () => {
|
||||
const response = await server.inject(req, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ROUTE_HANDLER_TIMEOUT } from '../constants';
|
|||
import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
|
||||
import { testPipeline } from '../util/pipeline';
|
||||
import { buildRouteValidationWithZod } from '../util/route_validation';
|
||||
import { withAvailability } from './with_availability';
|
||||
|
||||
export function registerPipelineRoutes(router: IRouter<IntegrationAssistantRouteHandlerContext>) {
|
||||
router.versioned
|
||||
|
@ -32,21 +33,23 @@ export function registerPipelineRoutes(router: IRouter<IntegrationAssistantRoute
|
|||
},
|
||||
},
|
||||
},
|
||||
async (context, req, res): Promise<IKibanaResponse<CheckPipelineResponse>> => {
|
||||
const { rawSamples, pipeline } = req.body;
|
||||
const services = await context.resolve(['core']);
|
||||
const { client } = services.core.elasticsearch;
|
||||
try {
|
||||
const { errors, pipelineResults } = await testPipeline(rawSamples, pipeline, client);
|
||||
if (errors?.length) {
|
||||
return res.badRequest({ body: JSON.stringify(errors) });
|
||||
withAvailability(
|
||||
async (context, req, res): Promise<IKibanaResponse<CheckPipelineResponse>> => {
|
||||
const { rawSamples, pipeline } = req.body;
|
||||
const services = await context.resolve(['core']);
|
||||
const { client } = services.core.elasticsearch;
|
||||
try {
|
||||
const { errors, pipelineResults } = await testPipeline(rawSamples, pipeline, client);
|
||||
if (errors?.length) {
|
||||
return res.badRequest({ body: JSON.stringify(errors) });
|
||||
}
|
||||
return res.ok({
|
||||
body: CheckPipelineResponse.parse({ results: { docs: pipelineResults } }),
|
||||
});
|
||||
} catch (e) {
|
||||
return res.badRequest({ body: e });
|
||||
}
|
||||
return res.ok({
|
||||
body: CheckPipelineResponse.parse({ results: { docs: pipelineResults } }),
|
||||
});
|
||||
} catch (e) {
|
||||
return res.badRequest({ body: e });
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -64,4 +64,15 @@ describe('registerRelatedRoutes', () => {
|
|||
const response = await server.inject(req, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
|
||||
describe('when the integration assistant is not available', () => {
|
||||
beforeEach(() => {
|
||||
context.integrationAssistant.isAvailable.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('returns a 404', async () => {
|
||||
const response = await server.inject(req, requestContextMock.convertContext(context));
|
||||
expect(response.status).toEqual(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ import { ROUTE_HANDLER_TIMEOUT } from '../constants';
|
|||
import { getRelatedGraph } from '../graphs/related';
|
||||
import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
|
||||
import { buildRouteValidationWithZod } from '../util/route_validation';
|
||||
import { withAvailability } from './with_availability';
|
||||
|
||||
export function registerRelatedRoutes(router: IRouter<IntegrationAssistantRouteHandlerContext>) {
|
||||
router.versioned
|
||||
|
@ -37,7 +38,7 @@ export function registerRelatedRoutes(router: IRouter<IntegrationAssistantRouteH
|
|||
},
|
||||
},
|
||||
},
|
||||
async (context, req, res): Promise<IKibanaResponse<RelatedResponse>> => {
|
||||
withAvailability(async (context, req, res): Promise<IKibanaResponse<RelatedResponse>> => {
|
||||
const { packageName, dataStreamName, rawSamples, currentPipeline } = req.body;
|
||||
const services = await context.resolve(['core']);
|
||||
const { client } = services.core.elasticsearch;
|
||||
|
@ -79,6 +80,6 @@ export function registerRelatedRoutes(router: IRouter<IntegrationAssistantRouteH
|
|||
} catch (e) {
|
||||
return res.badRequest({ body: e });
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { RequestHandler, RouteMethod } from '@kbn/core/server';
|
||||
import { IntegrationAssistantRouteHandlerContext } from '../plugin';
|
||||
|
||||
/**
|
||||
* Wraps a request handler with a check for whether the API route is available.
|
||||
* The `isAvailable` flag must be provided by the context and be consistent with the required
|
||||
* license (stateful) or product type (serverless).
|
||||
*/
|
||||
export const withAvailability = <
|
||||
P = unknown,
|
||||
Q = unknown,
|
||||
B = unknown,
|
||||
Method extends RouteMethod = never
|
||||
>(
|
||||
handler: RequestHandler<P, Q, B, IntegrationAssistantRouteHandlerContext, Method>
|
||||
): RequestHandler<P, Q, B, IntegrationAssistantRouteHandlerContext, Method> => {
|
||||
return async (context, req, res) => {
|
||||
const { isAvailable } = await context.integrationAssistant;
|
||||
if (!isAvailable()) {
|
||||
return res.notFound({
|
||||
body: { message: 'This API route is not available using your current license/tier.' },
|
||||
});
|
||||
}
|
||||
return handler(context, req, res);
|
||||
};
|
||||
};
|
|
@ -5,11 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface IntegrationAssistantPluginSetup {}
|
||||
import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server';
|
||||
|
||||
export interface IntegrationAssistantPluginSetup {
|
||||
setIsAvailable: (isAvailable: boolean) => void;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface IntegrationAssistantPluginStart {}
|
||||
|
||||
export interface IntegrationAssistantPluginSetupDependencies {
|
||||
licensing: LicensingPluginSetup;
|
||||
}
|
||||
export interface IntegrationAssistantPluginStartDependencies {
|
||||
licensing: LicensingPluginStart;
|
||||
}
|
||||
|
||||
export interface CategorizationState {
|
||||
rawSamples: string[];
|
||||
samples: string[];
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"@kbn/stack-connectors-plugin",
|
||||
"@kbn/core-analytics-browser",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/licensing-plugin",
|
||||
"@kbn/core-http-request-handler-context-server",
|
||||
"@kbn/core-http-router-server-mocks",
|
||||
"@kbn/core-http-server"
|
||||
|
|
|
@ -156,7 +156,7 @@ AddIntegrationPanel.displayName = 'AddIntegrationPanel';
|
|||
|
||||
export const AddIntegrationButtons: React.FC = React.memo(() => {
|
||||
const { integrationAssistant } = useKibana().services;
|
||||
const CreateIntegrationCardButton = integrationAssistant?.CreateIntegrationCardButton;
|
||||
const { CreateIntegrationCardButton } = integrationAssistant?.components ?? {};
|
||||
return (
|
||||
<EuiFlexGroup direction="column" className="step-paragraph" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -27,6 +27,7 @@ export const PLI_PRODUCT_FEATURES: PliProductFeatures = {
|
|||
ProductFeatureKey.threatIntelligence,
|
||||
ProductFeatureKey.casesConnectors,
|
||||
ProductFeatureKey.externalRuleActions,
|
||||
ProductFeatureKey.integrationAssistant,
|
||||
],
|
||||
},
|
||||
endpoint: {
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
"discover"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"securitySolutionEss"
|
||||
"securitySolutionEss",
|
||||
"integrationAssistant"
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,10 +64,9 @@ export class SecuritySolutionServerlessPlugin
|
|||
): SecuritySolutionServerlessPluginStart {
|
||||
const { securitySolution } = startDeps;
|
||||
const { productTypes } = this.config;
|
||||
|
||||
const services = createServices(core, startDeps, this.experimentalFeatures);
|
||||
|
||||
registerUpsellings(securitySolution.getUpselling(), productTypes, services);
|
||||
registerUpsellings(productTypes, services);
|
||||
|
||||
securitySolution.setComponents({
|
||||
DashboardsLandingCallout: getDashboardsLandingCallout(services),
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverle
|
|||
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import type { DiscoverSetup } from '@kbn/discover-plugin/public';
|
||||
import type { IntegrationAssistantPluginStart } from '@kbn/integration-assistant-plugin/public';
|
||||
import type { ServerlessSecurityConfigSchema } from '../common/config';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
|
@ -36,6 +37,7 @@ export interface SecuritySolutionServerlessPluginStartDeps {
|
|||
serverless: ServerlessPluginStart;
|
||||
management: ManagementStart;
|
||||
cloud: CloudStart;
|
||||
integrationAssistant?: IntegrationAssistantPluginStart;
|
||||
}
|
||||
|
||||
export type ServerlessSecurityPublicConfig = Pick<
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { ProductFeatureKeyType } from '@kbn/security-solution-features';
|
||||
import { useMemo } from 'react';
|
||||
import { PLI_PRODUCT_FEATURES } from '../../../common/pli/pli_config';
|
||||
|
||||
export const getProductTypeByPLI = (requiredPLI: ProductFeatureKeyType): string | null => {
|
||||
|
@ -29,3 +30,7 @@ export const getProductTypeByPLI = (requiredPLI: ProductFeatureKeyType): string
|
|||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useProductTypeByPLI = (requiredPLI: ProductFeatureKeyType): string => {
|
||||
return useMemo(() => getProductTypeByPLI(requiredPLI) ?? '', [requiredPLI]);
|
||||
};
|
||||
|
|
|
@ -5,23 +5,31 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
registerUpsellings,
|
||||
upsellingMessages,
|
||||
upsellingPages,
|
||||
upsellingSections,
|
||||
} from './register_upsellings';
|
||||
import { registerUpsellings } from './register_upsellings';
|
||||
import { upsellingMessages, upsellingPages, upsellingSections } from './upsellings';
|
||||
import { ProductLine, ProductTier } from '../../common/product';
|
||||
import type { SecurityProductTypes } from '../../common/config';
|
||||
import { ALL_PRODUCT_FEATURE_KEYS } from '@kbn/security-solution-features/keys';
|
||||
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
|
||||
import { mockServices } from '../common/services/__mocks__/services.mock';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
const mockGetProductProductFeatures = jest.fn();
|
||||
jest.mock('../../common/pli/pli_features', () => ({
|
||||
getProductProductFeatures: () => mockGetProductProductFeatures(),
|
||||
}));
|
||||
|
||||
const setPages = jest.fn();
|
||||
const setSections = jest.fn();
|
||||
const setMessages = jest.fn();
|
||||
const upselling = {
|
||||
setPages,
|
||||
setSections,
|
||||
setMessages,
|
||||
sections$: of([]),
|
||||
} as unknown as UpsellingService;
|
||||
mockServices.securitySolution.getUpselling = jest.fn(() => upselling);
|
||||
|
||||
const allProductTypes: SecurityProductTypes = [
|
||||
{ product_line: ProductLine.security, product_tier: ProductTier.complete },
|
||||
{ product_line: ProductLine.endpoint, product_tier: ProductTier.complete },
|
||||
|
@ -29,19 +37,14 @@ const allProductTypes: SecurityProductTypes = [
|
|||
];
|
||||
|
||||
describe('registerUpsellings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not register anything when all PLIs features are enabled', () => {
|
||||
mockGetProductProductFeatures.mockReturnValue(ALL_PRODUCT_FEATURE_KEYS);
|
||||
|
||||
const setPages = jest.fn();
|
||||
const setSections = jest.fn();
|
||||
const setMessages = jest.fn();
|
||||
const upselling = {
|
||||
setPages,
|
||||
setSections,
|
||||
setMessages,
|
||||
} as unknown as UpsellingService;
|
||||
|
||||
registerUpsellings(upselling, allProductTypes, mockServices);
|
||||
registerUpsellings(allProductTypes, mockServices);
|
||||
|
||||
expect(setPages).toHaveBeenCalledTimes(1);
|
||||
expect(setPages).toHaveBeenCalledWith({});
|
||||
|
@ -56,17 +59,7 @@ describe('registerUpsellings', () => {
|
|||
it('should register all upsellings pages, sections and messages when PLIs features are disabled', () => {
|
||||
mockGetProductProductFeatures.mockReturnValue([]);
|
||||
|
||||
const setPages = jest.fn();
|
||||
const setSections = jest.fn();
|
||||
const setMessages = jest.fn();
|
||||
|
||||
const upselling = {
|
||||
setPages,
|
||||
setSections,
|
||||
setMessages,
|
||||
} as unknown as UpsellingService;
|
||||
|
||||
registerUpsellings(upselling, allProductTypes, mockServices);
|
||||
registerUpsellings(allProductTypes, mockServices);
|
||||
|
||||
const expectedPagesObject = Object.fromEntries(
|
||||
upsellingPages.map(({ pageName }) => [pageName, expect.anything()])
|
||||
|
|
|
@ -4,63 +4,32 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { ProductFeatureKeyType } from '@kbn/security-solution-features';
|
||||
import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
|
||||
import { SecurityPageName } from '@kbn/security-solution-plugin/common';
|
||||
import {
|
||||
UPGRADE_INVESTIGATION_GUIDE,
|
||||
UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS,
|
||||
} from '@kbn/security-solution-upselling/messages';
|
||||
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
|
||||
import type {
|
||||
MessageUpsellings,
|
||||
PageUpsellings,
|
||||
SectionUpsellings,
|
||||
UpsellingMessageId,
|
||||
UpsellingSectionId,
|
||||
} from '@kbn/security-solution-upselling/service/types';
|
||||
import React from 'react';
|
||||
import { CloudSecurityPostureIntegrationPliBlockLazy } from './sections/cloud_security_posture';
|
||||
import {
|
||||
EndpointAgentTamperProtectionLazy,
|
||||
EndpointPolicyProtectionsLazy,
|
||||
EndpointProtectionUpdatesLazy,
|
||||
RuleDetailsEndpointExceptionsLazy,
|
||||
} from './sections/endpoint_management';
|
||||
import type { SecurityProductTypes } from '../../common/config';
|
||||
import { getProductProductFeatures } from '../../common/pli/pli_features';
|
||||
import type { Services } from '../common/services';
|
||||
import { withServicesProvider } from '../common/services';
|
||||
import { getProductTypeByPLI } from './hooks/use_product_type_by_pli';
|
||||
import {
|
||||
EndpointExceptionsDetailsUpsellingLazy,
|
||||
EntityAnalyticsUpsellingPageLazy,
|
||||
EntityAnalyticsUpsellingSectionLazy,
|
||||
OsqueryResponseActionsUpsellingSectionLazy,
|
||||
ThreatIntelligencePaywallLazy,
|
||||
} from './lazy_upselling';
|
||||
import * as i18n from './translations';
|
||||
import { upsellingPages, upsellingSections, upsellingMessages } from './upsellings';
|
||||
|
||||
interface UpsellingsConfig {
|
||||
pli: ProductFeatureKeyType;
|
||||
component: React.ComponentType;
|
||||
}
|
||||
export const registerUpsellings = (productTypes: SecurityProductTypes, services: Services) => {
|
||||
const upsellingService = registerSecuritySolutionUpsellings(productTypes, services);
|
||||
configurePluginsUpsellings(upsellingService, services);
|
||||
};
|
||||
|
||||
interface UpsellingsMessageConfig {
|
||||
pli: ProductFeatureKeyType;
|
||||
message: string;
|
||||
id: UpsellingMessageId;
|
||||
}
|
||||
|
||||
type UpsellingPages = Array<UpsellingsConfig & { pageName: SecurityPageName }>;
|
||||
type UpsellingSections = Array<UpsellingsConfig & { id: UpsellingSectionId }>;
|
||||
type UpsellingMessages = UpsellingsMessageConfig[];
|
||||
|
||||
export const registerUpsellings = (
|
||||
upselling: UpsellingService,
|
||||
/**
|
||||
* Registers the upsellings for the security solution.
|
||||
*/
|
||||
const registerSecuritySolutionUpsellings = (
|
||||
productTypes: SecurityProductTypes,
|
||||
services: Services
|
||||
) => {
|
||||
): UpsellingService => {
|
||||
const upsellingService = services.securitySolution.getUpselling();
|
||||
|
||||
const enabledPLIsSet = new Set(getProductProductFeatures(productTypes));
|
||||
|
||||
const upsellingPagesToRegister = upsellingPages.reduce<PageUpsellings>(
|
||||
|
@ -93,105 +62,20 @@ export const registerUpsellings = (
|
|||
{}
|
||||
);
|
||||
|
||||
upselling.setPages(upsellingPagesToRegister);
|
||||
upselling.setSections(upsellingSectionsToRegister);
|
||||
upselling.setMessages(upsellingMessagesToRegister);
|
||||
upsellingService.setPages(upsellingPagesToRegister);
|
||||
upsellingService.setSections(upsellingSectionsToRegister);
|
||||
upsellingService.setMessages(upsellingMessagesToRegister);
|
||||
|
||||
return upsellingService;
|
||||
};
|
||||
|
||||
// Upselling for entire pages, linked to a SecurityPageName
|
||||
export const upsellingPages: UpsellingPages = [
|
||||
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
|
||||
{
|
||||
pageName: SecurityPageName.entityAnalytics,
|
||||
pli: ProductFeatureKey.advancedInsights,
|
||||
component: () => (
|
||||
<EntityAnalyticsUpsellingPageLazy
|
||||
upgradeToLabel={entityAnalyticsProductType}
|
||||
upgradeMessage={i18n.UPGRADE_PRODUCT_MESSAGE(entityAnalyticsProductType)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
pageName: SecurityPageName.threatIntelligence,
|
||||
pli: ProductFeatureKey.threatIntelligence,
|
||||
component: () => (
|
||||
<ThreatIntelligencePaywallLazy requiredPLI={ProductFeatureKey.threatIntelligence} />
|
||||
),
|
||||
},
|
||||
{
|
||||
pageName: SecurityPageName.exceptions,
|
||||
pli: ProductFeatureKey.endpointExceptions,
|
||||
component: () => (
|
||||
<EndpointExceptionsDetailsUpsellingLazy requiredPLI={ProductFeatureKey.endpointExceptions} />
|
||||
),
|
||||
},
|
||||
];
|
||||
/**
|
||||
* Configures the upsellings for other plugins.
|
||||
*/
|
||||
const configurePluginsUpsellings = (upsellingService: UpsellingService, services: Services) => {
|
||||
const { integrationAssistant } = services;
|
||||
|
||||
const entityAnalyticsProductType = getProductTypeByPLI(ProductFeatureKey.advancedInsights) ?? '';
|
||||
|
||||
// Upselling for sections, linked by arbitrary ids
|
||||
export const upsellingSections: UpsellingSections = [
|
||||
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
|
||||
{
|
||||
id: 'osquery_automated_response_actions',
|
||||
pli: ProductFeatureKey.osqueryAutomatedResponseActions,
|
||||
component: () => (
|
||||
<OsqueryResponseActionsUpsellingSectionLazy
|
||||
requiredPLI={ProductFeatureKey.osqueryAutomatedResponseActions}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'endpoint_agent_tamper_protection',
|
||||
pli: ProductFeatureKey.endpointAgentTamperProtection,
|
||||
component: EndpointAgentTamperProtectionLazy,
|
||||
},
|
||||
{
|
||||
id: 'endpointPolicyProtections',
|
||||
pli: ProductFeatureKey.endpointPolicyProtections,
|
||||
component: EndpointPolicyProtectionsLazy,
|
||||
},
|
||||
{
|
||||
id: 'ruleDetailsEndpointExceptions',
|
||||
pli: ProductFeatureKey.endpointExceptions,
|
||||
component: RuleDetailsEndpointExceptionsLazy,
|
||||
},
|
||||
{
|
||||
id: 'endpoint_protection_updates',
|
||||
pli: ProductFeatureKey.endpointProtectionUpdates,
|
||||
component: EndpointProtectionUpdatesLazy,
|
||||
},
|
||||
{
|
||||
id: 'cloud_security_posture_integration_installation',
|
||||
pli: ProductFeatureKey.cloudSecurityPosture,
|
||||
component: CloudSecurityPostureIntegrationPliBlockLazy,
|
||||
},
|
||||
{
|
||||
id: 'entity_analytics_panel',
|
||||
pli: ProductFeatureKey.advancedInsights,
|
||||
component: () => (
|
||||
<EntityAnalyticsUpsellingSectionLazy
|
||||
upgradeToLabel={entityAnalyticsProductType}
|
||||
upgradeMessage={i18n.UPGRADE_PRODUCT_MESSAGE(entityAnalyticsProductType)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Upselling for sections, linked by arbitrary ids
|
||||
export const upsellingMessages: UpsellingMessages = [
|
||||
{
|
||||
id: 'investigation_guide',
|
||||
pli: ProductFeatureKey.investigationGuide,
|
||||
message: UPGRADE_INVESTIGATION_GUIDE(
|
||||
getProductTypeByPLI(ProductFeatureKey.investigationGuide) ?? ''
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'investigation_guide_interactions',
|
||||
pli: ProductFeatureKey.investigationGuideInteractions,
|
||||
message: UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS(
|
||||
getProductTypeByPLI(ProductFeatureKey.investigationGuideInteractions) ?? ''
|
||||
),
|
||||
},
|
||||
];
|
||||
upsellingService.sections$.subscribe((sections) => {
|
||||
integrationAssistant?.renderUpselling(sections.get('integration_assistant'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 { lazy } from 'react';
|
||||
|
||||
export const IntegrationsAssistantLazy = lazy(() =>
|
||||
import('./integration_assistant').then(({ IntegrationsAssistant }) => ({
|
||||
default: IntegrationsAssistant,
|
||||
}))
|
||||
);
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiCard,
|
||||
EuiIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ProductFeatureKeyType } from '@kbn/security-solution-features';
|
||||
import { useProductTypeByPLI } from '../../hooks/use_product_type_by_pli';
|
||||
|
||||
export const UPGRADE_PRODUCT_MESSAGE = (requiredProductType: string) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolutionServerless.upselling.integrationAssistant.upgradeProductMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'To turn on the Integration Assistant feature, you must upgrade the product tier to {requiredProductType}',
|
||||
values: {
|
||||
requiredProductType,
|
||||
},
|
||||
}
|
||||
);
|
||||
export const TIER_REQUIRED = (requiredProductType: string) =>
|
||||
i18n.translate('xpack.securitySolutionServerless.upselling.integrationAssistant.tierRequired', {
|
||||
defaultMessage: '{requiredProductType} tier required',
|
||||
values: {
|
||||
requiredProductType,
|
||||
},
|
||||
});
|
||||
export const CONTACT_ADMINISTRATOR = i18n.translate(
|
||||
'xpack.securitySolutionServerless.upselling.integrationAssistant.contactAdministrator',
|
||||
{
|
||||
defaultMessage: 'Contact your administrator for assistance.',
|
||||
}
|
||||
);
|
||||
|
||||
export interface IntegrationsAssistantProps {
|
||||
requiredPLI: ProductFeatureKeyType;
|
||||
}
|
||||
export const IntegrationsAssistant = React.memo<IntegrationsAssistantProps>(({ requiredPLI }) => {
|
||||
const requiredProductType = useProductTypeByPLI(requiredPLI);
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCard
|
||||
data-test-subj={'EnterpriseLicenseRequiredCard'}
|
||||
betaBadgeProps={{
|
||||
label: requiredProductType,
|
||||
}}
|
||||
isDisabled={true}
|
||||
icon={<EuiIcon size="xl" type="lock" />}
|
||||
title={
|
||||
<h3>
|
||||
<strong>{TIER_REQUIRED(requiredProductType)}</strong>
|
||||
</h3>
|
||||
}
|
||||
description={false}
|
||||
>
|
||||
<EuiFlexGroup className="lockedCardDescription" direction="column" justifyContent="center">
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText>
|
||||
<h4>
|
||||
<EuiTextColor color="subdued">
|
||||
{UPGRADE_PRODUCT_MESSAGE(requiredProductType)}
|
||||
</EuiTextColor>
|
||||
</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>{CONTACT_ADMINISTRATOR}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiCard>
|
||||
</>
|
||||
);
|
||||
});
|
||||
IntegrationsAssistant.displayName = 'IntegrationsAssistant';
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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 type { ProductFeatureKeyType } from '@kbn/security-solution-features';
|
||||
import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
|
||||
import { SecurityPageName } from '@kbn/security-solution-plugin/common';
|
||||
import {
|
||||
UPGRADE_INVESTIGATION_GUIDE,
|
||||
UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS,
|
||||
} from '@kbn/security-solution-upselling/messages';
|
||||
import type {
|
||||
UpsellingMessageId,
|
||||
UpsellingSectionId,
|
||||
} from '@kbn/security-solution-upselling/service/types';
|
||||
import React from 'react';
|
||||
import { CloudSecurityPostureIntegrationPliBlockLazy } from './sections/cloud_security_posture';
|
||||
import {
|
||||
EndpointAgentTamperProtectionLazy,
|
||||
EndpointPolicyProtectionsLazy,
|
||||
EndpointProtectionUpdatesLazy,
|
||||
RuleDetailsEndpointExceptionsLazy,
|
||||
} from './sections/endpoint_management';
|
||||
import { getProductTypeByPLI } from './hooks/use_product_type_by_pli';
|
||||
import {
|
||||
EndpointExceptionsDetailsUpsellingLazy,
|
||||
EntityAnalyticsUpsellingPageLazy,
|
||||
EntityAnalyticsUpsellingSectionLazy,
|
||||
OsqueryResponseActionsUpsellingSectionLazy,
|
||||
ThreatIntelligencePaywallLazy,
|
||||
} from './lazy_upselling';
|
||||
import * as i18n from './translations';
|
||||
import { IntegrationsAssistantLazy } from './sections/integration_assistant';
|
||||
|
||||
interface UpsellingsConfig {
|
||||
pli: ProductFeatureKeyType;
|
||||
component: React.ComponentType;
|
||||
}
|
||||
|
||||
interface UpsellingsMessageConfig {
|
||||
pli: ProductFeatureKeyType;
|
||||
message: string;
|
||||
id: UpsellingMessageId;
|
||||
}
|
||||
|
||||
type UpsellingPages = Array<UpsellingsConfig & { pageName: SecurityPageName }>;
|
||||
type UpsellingSections = Array<UpsellingsConfig & { id: UpsellingSectionId }>;
|
||||
type UpsellingMessages = UpsellingsMessageConfig[];
|
||||
|
||||
// Upselling for entire pages, linked to a SecurityPageName
|
||||
export const upsellingPages: UpsellingPages = [
|
||||
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
|
||||
{
|
||||
pageName: SecurityPageName.entityAnalytics,
|
||||
pli: ProductFeatureKey.advancedInsights,
|
||||
component: () => (
|
||||
<EntityAnalyticsUpsellingPageLazy
|
||||
upgradeToLabel={entityAnalyticsProductType}
|
||||
upgradeMessage={i18n.UPGRADE_PRODUCT_MESSAGE(entityAnalyticsProductType)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
pageName: SecurityPageName.threatIntelligence,
|
||||
pli: ProductFeatureKey.threatIntelligence,
|
||||
component: () => (
|
||||
<ThreatIntelligencePaywallLazy requiredPLI={ProductFeatureKey.threatIntelligence} />
|
||||
),
|
||||
},
|
||||
{
|
||||
pageName: SecurityPageName.exceptions,
|
||||
pli: ProductFeatureKey.endpointExceptions,
|
||||
component: () => (
|
||||
<EndpointExceptionsDetailsUpsellingLazy requiredPLI={ProductFeatureKey.endpointExceptions} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const entityAnalyticsProductType = getProductTypeByPLI(ProductFeatureKey.advancedInsights) ?? '';
|
||||
|
||||
// Upselling for sections, linked by arbitrary ids
|
||||
export const upsellingSections: UpsellingSections = [
|
||||
// It is highly advisable to make use of lazy loaded components to minimize bundle size.
|
||||
{
|
||||
id: 'osquery_automated_response_actions',
|
||||
pli: ProductFeatureKey.osqueryAutomatedResponseActions,
|
||||
component: () => (
|
||||
<OsqueryResponseActionsUpsellingSectionLazy
|
||||
requiredPLI={ProductFeatureKey.osqueryAutomatedResponseActions}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'endpoint_agent_tamper_protection',
|
||||
pli: ProductFeatureKey.endpointAgentTamperProtection,
|
||||
component: EndpointAgentTamperProtectionLazy,
|
||||
},
|
||||
{
|
||||
id: 'endpointPolicyProtections',
|
||||
pli: ProductFeatureKey.endpointPolicyProtections,
|
||||
component: EndpointPolicyProtectionsLazy,
|
||||
},
|
||||
{
|
||||
id: 'ruleDetailsEndpointExceptions',
|
||||
pli: ProductFeatureKey.endpointExceptions,
|
||||
component: RuleDetailsEndpointExceptionsLazy,
|
||||
},
|
||||
{
|
||||
id: 'endpoint_protection_updates',
|
||||
pli: ProductFeatureKey.endpointProtectionUpdates,
|
||||
component: EndpointProtectionUpdatesLazy,
|
||||
},
|
||||
{
|
||||
id: 'cloud_security_posture_integration_installation',
|
||||
pli: ProductFeatureKey.cloudSecurityPosture,
|
||||
component: CloudSecurityPostureIntegrationPliBlockLazy,
|
||||
},
|
||||
{
|
||||
id: 'entity_analytics_panel',
|
||||
pli: ProductFeatureKey.advancedInsights,
|
||||
component: () => (
|
||||
<EntityAnalyticsUpsellingSectionLazy
|
||||
upgradeToLabel={entityAnalyticsProductType}
|
||||
upgradeMessage={i18n.UPGRADE_PRODUCT_MESSAGE(entityAnalyticsProductType)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'integration_assistant',
|
||||
pli: ProductFeatureKey.integrationAssistant,
|
||||
component: () => (
|
||||
<IntegrationsAssistantLazy requiredPLI={ProductFeatureKey.integrationAssistant} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Upselling for sections, linked by arbitrary ids
|
||||
export const upsellingMessages: UpsellingMessages = [
|
||||
{
|
||||
id: 'investigation_guide',
|
||||
pli: ProductFeatureKey.investigationGuide,
|
||||
message: UPGRADE_INVESTIGATION_GUIDE(
|
||||
getProductTypeByPLI(ProductFeatureKey.investigationGuide) ?? ''
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'investigation_guide_interactions',
|
||||
pli: ProductFeatureKey.investigationGuideInteractions,
|
||||
message: UPGRADE_INVESTIGATION_GUIDE_INTERACTIONS(
|
||||
getProductTypeByPLI(ProductFeatureKey.investigationGuideInteractions) ?? ''
|
||||
),
|
||||
},
|
||||
];
|
|
@ -26,13 +26,12 @@ import type {
|
|||
} from './types';
|
||||
import { SecurityUsageReportingTask } from './task_manager/usage_reporting_task';
|
||||
import { cloudSecurityMetringTaskProperties } from './cloud_security/cloud_security_metering_task_config';
|
||||
import { getProductProductFeaturesConfigurator, getSecurityProductTier } from './product_features';
|
||||
import { registerProductFeatures, getSecurityProductTier } from './product_features';
|
||||
import { METERING_TASK as ENDPOINT_METERING_TASK } from './endpoint/constants/metering';
|
||||
import {
|
||||
endpointMeteringService,
|
||||
setEndpointPackagePolicyServerlessBillingFlags,
|
||||
} from './endpoint/services';
|
||||
import { enableRuleActions } from './rules/enable_rule_actions';
|
||||
import { NLPCleanupTask } from './task_manager/nlp_cleanup_task/nlp_cleanup_task';
|
||||
import { telemetryEvents } from './telemetry/event_based_telemetry';
|
||||
|
||||
|
@ -54,34 +53,25 @@ export class SecuritySolutionServerlessPlugin
|
|||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<ServerlessSecurityConfig>();
|
||||
this.logger = this.initializerContext.logger.get();
|
||||
|
||||
const productTypesStr = JSON.stringify(this.config.productTypes, null, 2);
|
||||
this.logger.info(`Security Solution running with product types:\n${productTypesStr}`);
|
||||
}
|
||||
|
||||
public setup(coreSetup: CoreSetup, pluginsSetup: SecuritySolutionServerlessPluginSetupDeps) {
|
||||
this.config = createConfig(this.initializerContext, pluginsSetup.securitySolution);
|
||||
const enabledProductFeatures = getProductProductFeatures(this.config.productTypes);
|
||||
|
||||
// securitySolutionEss plugin should always be disabled when securitySolutionServerless is enabled.
|
||||
// This check is an additional layer of security to prevent double registrations when
|
||||
// `plugins.forceEnableAllPlugins` flag is enabled. Should never happen in real scenarios.
|
||||
const shouldRegister = pluginsSetup.securitySolutionEss == null;
|
||||
if (shouldRegister) {
|
||||
const productTypesStr = JSON.stringify(this.config.productTypes, null, 2);
|
||||
this.logger.info(`Security Solution running with product types:\n${productTypesStr}`);
|
||||
const productFeaturesConfigurator = getProductProductFeaturesConfigurator(
|
||||
enabledProductFeatures,
|
||||
this.config
|
||||
);
|
||||
pluginsSetup.securitySolution.setProductFeaturesConfigurator(productFeaturesConfigurator);
|
||||
}
|
||||
// Register product features
|
||||
const enabledProductFeatures = getProductProductFeatures(this.config.productTypes);
|
||||
registerProductFeatures(pluginsSetup, enabledProductFeatures, this.config);
|
||||
|
||||
// Register telemetry events
|
||||
telemetryEvents.forEach((eventConfig) => coreSetup.analytics.registerEventType(eventConfig));
|
||||
|
||||
enableRuleActions({
|
||||
actions: pluginsSetup.actions,
|
||||
productFeatureKeys: enabledProductFeatures,
|
||||
});
|
||||
// Setup project uiSettings whitelisting
|
||||
pluginsSetup.serverless.setupProjectSettings(SECURITY_PROJECT_SETTINGS);
|
||||
|
||||
// Tasks
|
||||
this.cloudSecurityUsageReportingTask = new SecurityUsageReportingTask({
|
||||
core: coreSetup,
|
||||
logFactory: this.initializerContext.logger,
|
||||
|
@ -113,8 +103,6 @@ export class SecuritySolutionServerlessPlugin
|
|||
taskManager: pluginsSetup.taskManager,
|
||||
});
|
||||
|
||||
pluginsSetup.serverless.setupProjectSettings(SECURITY_PROJECT_SETTINGS);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,30 +5,56 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ProductFeatureKeys } from '@kbn/security-solution-features';
|
||||
import type { ProductFeaturesConfigurator } from '@kbn/security-solution-plugin/server/lib/product_features_service/types';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { ServerlessSecurityConfig } from '../config';
|
||||
|
||||
import { ProductFeatureKey } from '@kbn/security-solution-features/keys';
|
||||
import type { ProductFeatureKeys } from '@kbn/security-solution-features';
|
||||
import { getCasesProductFeaturesConfigurator } from './cases_product_features_config';
|
||||
import { getSecurityProductFeaturesConfigurator } from './security_product_features_config';
|
||||
import { getSecurityAssistantProductFeaturesConfigurator } from './assistant_product_features_config';
|
||||
import type { Tier } from '../types';
|
||||
import { enableRuleActions } from '../rules/enable_rule_actions';
|
||||
import type { ServerlessSecurityConfig } from '../config';
|
||||
import type { Tier, SecuritySolutionServerlessPluginSetupDeps } from '../types';
|
||||
import { ProductLine } from '../../common/product';
|
||||
|
||||
export const getProductProductFeaturesConfigurator = (
|
||||
export const registerProductFeatures = (
|
||||
pluginsSetup: SecuritySolutionServerlessPluginSetupDeps,
|
||||
enabledProductFeatureKeys: ProductFeatureKeys,
|
||||
config: ServerlessSecurityConfig
|
||||
): ProductFeaturesConfigurator => {
|
||||
return {
|
||||
): void => {
|
||||
// securitySolutionEss plugin should always be disabled when securitySolutionServerless is enabled.
|
||||
// This check is an additional layer of security to prevent double registrations when
|
||||
// `plugins.forceEnableAllPlugins` flag is enabled. Should never happen in real scenarios.
|
||||
const shouldRegister = pluginsSetup.securitySolutionEss == null;
|
||||
if (!shouldRegister) {
|
||||
return;
|
||||
}
|
||||
|
||||
// register product features for the main security solution product features service
|
||||
pluginsSetup.securitySolution.setProductFeaturesConfigurator({
|
||||
security: getSecurityProductFeaturesConfigurator(
|
||||
enabledProductFeatureKeys,
|
||||
config.experimentalFeatures
|
||||
),
|
||||
cases: getCasesProductFeaturesConfigurator(enabledProductFeatureKeys),
|
||||
securityAssistant: getSecurityAssistantProductFeaturesConfigurator(enabledProductFeatureKeys),
|
||||
};
|
||||
});
|
||||
|
||||
// enable rule actions based on the enabled product features
|
||||
enableRuleActions({
|
||||
actions: pluginsSetup.actions,
|
||||
productFeatureKeys: enabledProductFeatureKeys,
|
||||
});
|
||||
|
||||
// set availability for the integration assistant plugin based on the product features
|
||||
pluginsSetup.integrationAssistant?.setIsAvailable(
|
||||
enabledProductFeatureKeys.includes(ProductFeatureKey.integrationAssistant)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the security product tier from the security product type in the config
|
||||
*/
|
||||
export const getSecurityProductTier = (config: ServerlessSecurityConfig, logger: Logger): Tier => {
|
||||
const securityProductType = config.productTypes.find(
|
||||
(productType) => productType.product_line === ProductLine.security
|
||||
|
|
|
@ -21,6 +21,7 @@ import type { FleetStartContract } from '@kbn/fleet-plugin/server';
|
|||
import type { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server';
|
||||
|
||||
import type { ServerlessPluginSetup } from '@kbn/serverless/server';
|
||||
import type { IntegrationAssistantPluginSetup } from '@kbn/integration-assistant-plugin/server';
|
||||
import type { ProductTier } from '../common/product';
|
||||
|
||||
import type { ServerlessSecurityConfig } from './config';
|
||||
|
@ -39,6 +40,7 @@ export interface SecuritySolutionServerlessPluginSetupDeps {
|
|||
taskManager: TaskManagerSetupContract;
|
||||
cloud: CloudSetup;
|
||||
actions: ActionsPluginSetupContract;
|
||||
integrationAssistant?: IntegrationAssistantPluginSetup;
|
||||
}
|
||||
|
||||
export interface SecuritySolutionServerlessPluginStartDeps {
|
||||
|
|
|
@ -44,5 +44,6 @@
|
|||
"@kbn/management-cards-navigation",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/logging",
|
||||
"@kbn/integration-assistant-plugin",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue