[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:
Ryland Herrick 2020-03-23 11:10:40 -05:00 committed by GitHub
parent cca23c26fc
commit 21e8cea183
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 92 additions and 23 deletions

View file

@ -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

View file

@ -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

View file

@ -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),
}}

View file

@ -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.',
}
);

View file

@ -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']}>