[SIEM] Add license check to ML Rule form (#60691)
* Gate ML Rules behind a license check If they don't have a Platinum or Trial license, then we disable the ML Card and provide them a link to the subscriptions marketing page. * Add aria-describedby for new ML input fields * Add data-test-subj to new ML input fields * Remove unused prop This is already passed as isLoading * Fix capitalization on translation id * Declare defaulted props as optional * Gray out entire ML card when ML Rules are disabled If we're editing an existing rule, or if the user has an insufficient license, we disable both the card and its selectability. This is more visually striking, and a more obvious CTA.
This commit is contained in:
parent
cca23c26fc
commit
21e8cea183
|
@ -10,12 +10,16 @@ import { EuiFlexGrid, EuiFlexItem, EuiRange, EuiFormRow } from '@elastic/eui';
|
|||
import { FieldHook } from '../../../../../shared_imports';
|
||||
|
||||
interface AnomalyThresholdSliderProps {
|
||||
describedByIds: string[];
|
||||
field: FieldHook;
|
||||
}
|
||||
type Event = React.ChangeEvent<HTMLInputElement>;
|
||||
type EventArg = Event | React.MouseEvent<HTMLButtonElement>;
|
||||
|
||||
export const AnomalyThresholdSlider: React.FC<AnomalyThresholdSliderProps> = ({ field }) => {
|
||||
export const AnomalyThresholdSlider: React.FC<AnomalyThresholdSliderProps> = ({
|
||||
describedByIds = [],
|
||||
field,
|
||||
}) => {
|
||||
const threshold = field.value as number;
|
||||
const onThresholdChange = useCallback(
|
||||
(event: EventArg) => {
|
||||
|
@ -26,7 +30,12 @@ export const AnomalyThresholdSlider: React.FC<AnomalyThresholdSliderProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow label={field.label} fullWidth>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={field.label}
|
||||
data-test-subj="anomalyThresholdSlider"
|
||||
describedByIds={describedByIds}
|
||||
>
|
||||
<EuiFlexGrid columns={2}>
|
||||
<EuiFlexItem>
|
||||
<EuiRange
|
||||
|
|
|
@ -20,10 +20,11 @@ const JobDisplay = ({ title, description }: { title: string; description: string
|
|||
);
|
||||
|
||||
interface MlJobSelectProps {
|
||||
describedByIds: string[];
|
||||
field: FieldHook;
|
||||
}
|
||||
|
||||
export const MlJobSelect: React.FC<MlJobSelectProps> = ({ field }) => {
|
||||
export const MlJobSelect: React.FC<MlJobSelectProps> = ({ describedByIds = [], field }) => {
|
||||
const jobId = field.value as string;
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
const [isLoading, siemJobs] = useSiemJobs(false);
|
||||
|
@ -41,7 +42,14 @@ export const MlJobSelect: React.FC<MlJobSelectProps> = ({ field }) => {
|
|||
}));
|
||||
|
||||
return (
|
||||
<EuiFormRow fullWidth label={field.label} isInvalid={isInvalid} error={errorMessage}>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={field.label}
|
||||
isInvalid={isInvalid}
|
||||
error={errorMessage}
|
||||
data-test-subj="mlJobSelect"
|
||||
describedByIds={describedByIds}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiSuperSelect
|
||||
|
|
|
@ -5,19 +5,58 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiIcon, EuiFormRow } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiCard,
|
||||
EuiFlexGrid,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FieldHook } from '../../../../../shared_imports';
|
||||
import { RuleType } from '../../../../../containers/detection_engine/rules/types';
|
||||
import * as i18n from './translations';
|
||||
import { isMlRule } from '../../helpers';
|
||||
|
||||
const MlCardDescription = ({ hasValidLicense = false }: { hasValidLicense?: boolean }) => (
|
||||
<EuiText size="s">
|
||||
{hasValidLicense ? (
|
||||
i18n.ML_TYPE_DESCRIPTION
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription"
|
||||
defaultMessage="Access to ML requires a {subscriptionsLink}."
|
||||
values={{
|
||||
subscriptionsLink: (
|
||||
<EuiLink href="https://www.elastic.co/subscriptions" target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.siem.components.stepDefineRule.ruleTypeField.subscriptionsLink"
|
||||
defaultMessage="Platinum subscription"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
interface SelectRuleTypeProps {
|
||||
describedByIds?: string[];
|
||||
field: FieldHook;
|
||||
isReadOnly: boolean;
|
||||
hasValidLicense?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ field, isReadOnly = false }) => {
|
||||
export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({
|
||||
describedByIds = [],
|
||||
field,
|
||||
hasValidLicense = false,
|
||||
isReadOnly = false,
|
||||
}) => {
|
||||
const ruleType = field.value as RuleType;
|
||||
const setType = useCallback(
|
||||
(type: RuleType) => {
|
||||
|
@ -27,10 +66,15 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ field, isReadOnl
|
|||
);
|
||||
const setMl = useCallback(() => setType('machine_learning'), [setType]);
|
||||
const setQuery = useCallback(() => setType('query'), [setType]);
|
||||
const license = true; // TODO
|
||||
const mlCardDisabled = isReadOnly || !hasValidLicense;
|
||||
|
||||
return (
|
||||
<EuiFormRow label={field.label} fullWidth>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
data-test-subj="selectRuleType"
|
||||
describedByIds={describedByIds}
|
||||
label={field.label}
|
||||
>
|
||||
<EuiFlexGrid columns={4}>
|
||||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
|
@ -47,11 +91,11 @@ export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ field, isReadOnl
|
|||
<EuiFlexItem>
|
||||
<EuiCard
|
||||
title={i18n.ML_TYPE_TITLE}
|
||||
description={license ? i18n.ML_TYPE_DESCRIPTION : i18n.ML_TYPE_DISABLED_DESCRIPTION}
|
||||
isDisabled={!license}
|
||||
description={<MlCardDescription hasValidLicense={hasValidLicense} />}
|
||||
icon={<EuiIcon size="l" type="machineLearningApp" />}
|
||||
isDisabled={mlCardDisabled}
|
||||
selectable={{
|
||||
isDisabled: isReadOnly,
|
||||
isDisabled: mlCardDisabled,
|
||||
onClick: setMl,
|
||||
isSelected: isMlRule(ruleType),
|
||||
}}
|
||||
|
|
|
@ -33,10 +33,3 @@ export const ML_TYPE_DESCRIPTION = i18n.translate(
|
|||
defaultMessage: 'Select ML job to detect anomalous activity.',
|
||||
}
|
||||
);
|
||||
|
||||
export const ML_TYPE_DISABLED_DESCRIPTION = i18n.translate(
|
||||
'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription',
|
||||
{
|
||||
defaultMessage: 'Access to ML requires a Platinum subscription.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -13,13 +13,14 @@ import {
|
|||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { FC, memo, useCallback, useState, useEffect } from 'react';
|
||||
import React, { FC, memo, useCallback, useState, useEffect, useContext } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public';
|
||||
import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules';
|
||||
import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants';
|
||||
import { MlCapabilitiesContext } from '../../../../../components/ml/permissions/ml_capabilities_provider';
|
||||
import { useUiSetting$ } from '../../../../../lib/kibana';
|
||||
import { setFieldValue, isMlRule } from '../../helpers';
|
||||
import * as RuleI18n from '../../translations';
|
||||
|
@ -103,6 +104,7 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
setForm,
|
||||
setStepData,
|
||||
}) => {
|
||||
const mlCapabilities = useContext(MlCapabilitiesContext);
|
||||
const [openTimelineSearch, setOpenTimelineSearch] = useState(false);
|
||||
const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false);
|
||||
const [localIsMlRule, setIsMlRule] = useState(false);
|
||||
|
@ -182,6 +184,8 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
path="ruleType"
|
||||
component={SelectRuleType}
|
||||
componentProps={{
|
||||
describedByIds: ['detectionEngineStepDefineRuleType'],
|
||||
hasValidLicense: mlCapabilities.isPlatinumOrTrialLicense,
|
||||
isReadOnly: isUpdateView,
|
||||
}}
|
||||
/>
|
||||
|
@ -220,7 +224,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
component={QueryBarDefineRule}
|
||||
componentProps={{
|
||||
browserFields,
|
||||
loading: indexPatternLoadingQueryBar,
|
||||
idAria: 'detectionEngineStepDefineRuleQueryBar',
|
||||
indexPattern: indexPatternQueryBar,
|
||||
isDisabled: isLoading,
|
||||
|
@ -234,8 +237,20 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({
|
|||
</EuiFormRow>
|
||||
<EuiFormRow fullWidth style={{ display: localIsMlRule ? 'flex' : 'none' }}>
|
||||
<>
|
||||
<UseField path="machineLearningJobId" component={MlJobSelect} />
|
||||
<UseField path="anomalyThreshold" component={AnomalyThresholdSlider} />
|
||||
<UseField
|
||||
path="machineLearningJobId"
|
||||
component={MlJobSelect}
|
||||
componentProps={{
|
||||
describedByIds: ['detectionEngineStepDefineRulemachineLearningJobId'],
|
||||
}}
|
||||
/>
|
||||
<UseField
|
||||
path="anomalyThreshold"
|
||||
component={AnomalyThresholdSlider}
|
||||
componentProps={{
|
||||
describedByIds: ['detectionEngineStepDefineRuleAnomalyThreshold'],
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</EuiFormRow>
|
||||
<FormDataProvider pathsToWatch={['index', 'ruleType']}>
|
||||
|
|
Loading…
Reference in a new issue