From 90e35a04bc795aebdd62c11cd53e279b566582b8 Mon Sep 17 00:00:00 2001 From: Nikita Indik Date: Sat, 14 Dec 2024 12:40:45 +0100 Subject: [PATCH] [Security Solution] Reduce dropdown options and improve tooltip texts in Rule Upgrade flyout (#203222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Partially addresses: #171520** ## Summary This PR updates the tooltips for the ‘Diff view’ and ‘Final update’ sections in the prebuilt rule upgrade flyout. It also streamlines the version picker by removing redundant options, making the UI simpler and clearer for users. ## Changes - Reduced the number of version picker items based on the diff outcome. Updated item names for better clarity. - Revised the tooltip text for the ‘Diff view’ section to better explain the available dropdown options. The tooltip now describes only the options in the dropdown to avoid overwhelming the user with unrelated information. - Updated the tooltip text for the ‘Final update’ section. ## Screenshots Scherm­afbeelding 2024-12-11 om 11 54 48 Scherm­afbeelding 2024-12-11 om 11 55 32 Scherm­afbeelding 2024-12-11 om 11 55 58 Work started on 06-Dec-2024. --------- Co-authored-by: Maxim Palenov --- .../comparison_side_help_info.tsx | 74 +++------- .../comparison_side/field_comparison_side.tsx | 50 +++++-- .../comparison_side/translations.ts | 73 ++++++++++ .../three_way_diff/comparison_side/utils.ts | 135 +++++++++++++++++- .../versions_picker/constants.ts | 45 ------ .../versions_picker.stories.tsx | 35 ++--- .../versions_picker/versions_picker.tsx | 48 ++++--- .../components/field_final_side_help_info.tsx | 8 +- .../rule_upgrade/field_upgrade_context.tsx | 21 ++- .../field_upgrade_state_info.tsx | 7 + .../field_upgrade_state_info/translations.tsx | 15 ++ .../field_upgrade_state_enum.ts | 1 + .../use_prebuilt_rules_upgrade_state.ts | 18 ++- 13 files changed, 365 insertions(+), 165 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx index 47e5e537f3ab0..37b23652bfcbe 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/comparison_side_help_info.tsx @@ -9,14 +9,10 @@ import React from 'react'; import useToggle from 'react-use/lib/useToggle'; import { EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; import { TITLE } from './translations'; -import { - BASE_VERSION, - CURRENT_VERSION, - FINAL_VERSION, - TARGET_VERSION, -} from './versions_picker/translations'; +import type { VersionsPickerOptionEnum } from './versions_picker/versions_picker'; +import { useFieldUpgradeContext } from '../rule_upgrade/field_upgrade_context'; +import { getOptionDetails } from './utils'; /** * Theme doesn't expose width variables. Using provided size variables will require @@ -27,9 +23,18 @@ import { */ const POPOVER_WIDTH = 320; -export function ComparisonSideHelpInfo(): JSX.Element { +interface ComparisonSideHelpInfoProps { + options: VersionsPickerOptionEnum[]; +} + +export function ComparisonSideHelpInfo({ options }: ComparisonSideHelpInfoProps): JSX.Element { const [isPopoverOpen, togglePopover] = useToggle(false); + const { hasResolvedValueDifferentFromSuggested } = useFieldUpgradeContext(); + const optionsWithDescriptions = options.map((option) => + getOptionDetails(option, hasResolvedValueDifferentFromSuggested) + ); + const button = ( {TITLE}, versions: ( <>
    -
  • - {BASE_VERSION} {'-'} {BASE_VERSION_EXPLANATION} -
  • -
  • - {CURRENT_VERSION} {'-'} {CURRENT_VERSION_EXPLANATION} -
  • -
  • - {TARGET_VERSION} {'-'} {TARGET_VERSION_EXPLANATION} -
  • -
  • - {FINAL_VERSION} {'-'} {FINAL_VERSION_EXPLANATION} -
  • + {optionsWithDescriptions.map( + ({ title: displayName, description: explanation }) => ( +
  • + {displayName} {'-'} {explanation} +
  • + ) + )}
), @@ -71,35 +71,3 @@ export function ComparisonSideHelpInfo(): JSX.Element { ); } - -const BASE_VERSION_EXPLANATION = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.baseVersionExplanation', - { - defaultMessage: 'version originally installed from Elastic prebuilt rules package', - } -); - -const CURRENT_VERSION_EXPLANATION = ( - {BASE_VERSION}, - }} - /> -); - -const TARGET_VERSION_EXPLANATION = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.targetVersionExplanation', - { - defaultMessage: 'version coming from a new version of Elastic prebuilt rules package', - } -); - -const FINAL_VERSION_EXPLANATION = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.finalVersionExplanation', - { - defaultMessage: - 'version used to the update the rule. Initial value is suggested by the diff algorithm.', - } -); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/field_comparison_side.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/field_comparison_side.tsx index 0b9b37c91e226..3f30ead78a73f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/field_comparison_side.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/field_comparison_side.tsx @@ -5,35 +5,56 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import { VersionsPicker } from './versions_picker/versions_picker'; -import type { Version } from './versions_picker/constants'; -import { SelectedVersions } from './versions_picker/constants'; +import { isEqual } from 'lodash'; +import usePrevious from 'react-use/lib/usePrevious'; +import { VersionsPicker, VersionsPickerOptionEnum } from './versions_picker/versions_picker'; import { FieldUpgradeSideHeader } from '../field_upgrade_side_header'; import { useFieldUpgradeContext } from '../rule_upgrade/field_upgrade_context'; -import { pickFieldValueForVersion } from './utils'; +import { + getComparisonOptionsForDiffOutcome, + getVersionsForComparison, + pickFieldValueForVersion, +} from './utils'; import { getSubfieldChanges } from './get_subfield_changes'; import { SubfieldChanges } from './subfield_changes'; import { ComparisonSideHelpInfo } from './comparison_side_help_info'; import * as i18n from './translations'; export function FieldComparisonSide(): JSX.Element { - const { fieldName, fieldDiff, finalDiffableRule } = useFieldUpgradeContext(); + const { fieldName, fieldDiff, finalDiffableRule, hasResolvedValueDifferentFromSuggested } = + useFieldUpgradeContext(); const resolvedValue = finalDiffableRule[fieldName]; - const [selectedVersions, setSelectedVersions] = useState( - SelectedVersions.CurrentFinal + const options = getComparisonOptionsForDiffOutcome( + fieldDiff.diff_outcome, + fieldDiff.conflict, + hasResolvedValueDifferentFromSuggested ); + const [selectedOption, setSelectedOption] = useState(options[0]); - const [oldVersionType, newVersionType] = selectedVersions.split('_') as [Version, Version]; + const [oldVersionType, newVersionType] = getVersionsForComparison( + selectedOption, + fieldDiff.has_base_version + ); const oldFieldValue = pickFieldValueForVersion(oldVersionType, fieldDiff, resolvedValue); - const newFieldValue = pickFieldValueForVersion(newVersionType, fieldDiff, resolvedValue); const subfieldChanges = getSubfieldChanges(fieldName, oldFieldValue, newFieldValue); + /* Change selected option to "My changes" if user has modified resolved value */ + const prevResolvedValue = usePrevious(resolvedValue); + useEffect(() => { + if ( + selectedOption !== VersionsPickerOptionEnum.MyChanges && + !isEqual(prevResolvedValue, resolvedValue) + ) { + setSelectedOption(VersionsPickerOptionEnum.MyChanges); + } + }, [hasResolvedValueDifferentFromSuggested, selectedOption, prevResolvedValue, resolvedValue]); + return ( <> @@ -42,15 +63,16 @@ export function FieldComparisonSide(): JSX.Element {

{i18n.TITLE} - +

diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts index 8208892ac298d..808ce32e7b88a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/translations.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { FINAL_UPDATE } from '../field_final_side/components/translations'; export const TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.upgradeRules.comparisonSide.title', @@ -20,3 +21,75 @@ export const NO_CHANGES = i18n.translate( defaultMessage: 'No changes', } ); + +export const UPDATE_FROM_ELASTIC_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.updateFromElasticTitle', + { + defaultMessage: 'Update from Elastic', + } +); + +export const UPDATE_FROM_ELASTIC_EXPLANATION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.updateFromElasticExplanation', + { + defaultMessage: 'view the changes in Elastic’s latest update', + } +); + +export const MY_CHANGES_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myChangesTitle', + { + defaultMessage: 'My changes', + } +); + +export const MY_CHANGES_EXPLANATION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myChangesExplanation', + { + defaultMessage: `view what you have changed in your installed rule and in the {finalUpdateSectionLabel} section`, + values: { + finalUpdateSectionLabel: FINAL_UPDATE, + }, + } +); + +export const MY_CHANGES_IN_RULE_UPGRADE_WORKFLOW_EXPLANATION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myChangesFinalUpdateOnlyExplanation', + { + defaultMessage: `view the changes you made in the {finalUpdateSectionLabel} section`, + values: { + finalUpdateSectionLabel: FINAL_UPDATE, + }, + } +); + +export const MERGED_CHANGES_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.mergedChangesTitle', + { + defaultMessage: 'My changes merged with Elastic’s', + } +); + +export const MERGED_CHANGES_EXPLANATION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.mergedChangesExplanation', + { + defaultMessage: 'view an update suggestion that combines your changes with Elastic’s', + } +); + +export const MY_ORIGINAL_CHANGES_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myOriginalChangesTitle', + { + defaultMessage: 'My original changes', + } +); + +export const MY_ORIGINAL_CHANGES_EXPLANATION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.myCustomizationExplanation', + { + defaultMessage: `view what you have changed in your installed rule. Doesn’t include changes made in the {finalUpdateSectionLabel} section.`, + values: { + finalUpdateSectionLabel: FINAL_UPDATE, + }, + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/utils.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/utils.ts index 8f97d3462d358..23bd893ba1e97 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/utils.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/utils.ts @@ -7,7 +7,14 @@ import stringify from 'json-stable-stringify'; import { Version } from './versions_picker/constants'; -import type { ThreeWayDiff } from '../../../../../../../common/api/detection_engine'; +import { + ThreeWayDiffOutcome, + type ThreeWayDiff, + ThreeWayDiffConflict, +} from '../../../../../../../common/api/detection_engine'; +import { VersionsPickerOptionEnum } from './versions_picker/versions_picker'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import * as i18n from './translations'; /** * Picks the field value for a given version either from a three-way diff object or from a user-set resolved value. @@ -44,3 +51,129 @@ export const stringifyToSortedJson = (fieldValue: unknown): string => { return stringify(fieldValue, { space: 2 }); }; + +interface OptionDetails { + title: string; + description: string; +} + +/** + * Returns the title and description for a given versions picker option. + */ +export function getOptionDetails( + option: VersionsPickerOptionEnum, + hasResolvedValueDifferentFromSuggested: boolean +): OptionDetails { + switch (option) { + case VersionsPickerOptionEnum.MyChanges: + return hasResolvedValueDifferentFromSuggested + ? { + title: i18n.MY_CHANGES_TITLE, + description: i18n.MY_CHANGES_IN_RULE_UPGRADE_WORKFLOW_EXPLANATION, + } + : { + title: i18n.MY_CHANGES_TITLE, + description: i18n.MY_CHANGES_EXPLANATION, + }; + case VersionsPickerOptionEnum.MyOriginalChanges: + return { + title: i18n.MY_ORIGINAL_CHANGES_TITLE, + description: i18n.MY_ORIGINAL_CHANGES_EXPLANATION, + }; + case VersionsPickerOptionEnum.UpdateFromElastic: + return { + title: i18n.UPDATE_FROM_ELASTIC_TITLE, + description: i18n.UPDATE_FROM_ELASTIC_EXPLANATION, + }; + case VersionsPickerOptionEnum.Merged: + return { + title: i18n.MERGED_CHANGES_TITLE, + description: i18n.MERGED_CHANGES_EXPLANATION, + }; + default: + return assertUnreachable(option); + } +} + +/** + * Returns the versions to be compared based on the selected versions picker option. + */ +export function getVersionsForComparison( + selectedOption: VersionsPickerOptionEnum, + hasBaseVersion: boolean +): [Version, Version] { + switch (selectedOption) { + case VersionsPickerOptionEnum.MyChanges: + return hasBaseVersion ? [Version.Base, Version.Final] : [Version.Current, Version.Final]; + case VersionsPickerOptionEnum.MyOriginalChanges: + return [Version.Base, Version.Current]; + case VersionsPickerOptionEnum.UpdateFromElastic: + return hasBaseVersion ? [Version.Base, Version.Target] : [Version.Current, Version.Target]; + case VersionsPickerOptionEnum.Merged: + return [Version.Base, Version.Target]; + default: + return assertUnreachable(selectedOption); + } +} + +/** + * Returns the versions picker options available for a given field diff outcome. + */ +export const getComparisonOptionsForDiffOutcome = ( + diffOutcome: ThreeWayDiffOutcome, + conflict: ThreeWayDiffConflict, + hasResolvedValueDifferentFromSuggested: boolean +): VersionsPickerOptionEnum[] => { + switch (diffOutcome) { + case ThreeWayDiffOutcome.StockValueCanUpdate: { + const options = []; + + if (hasResolvedValueDifferentFromSuggested) { + options.push(VersionsPickerOptionEnum.MyChanges); + } + options.push(VersionsPickerOptionEnum.UpdateFromElastic); + + return options; + } + case ThreeWayDiffOutcome.CustomizedValueNoUpdate: + return [VersionsPickerOptionEnum.MyChanges]; + case ThreeWayDiffOutcome.CustomizedValueSameUpdate: + return [VersionsPickerOptionEnum.MyChanges, VersionsPickerOptionEnum.UpdateFromElastic]; + case ThreeWayDiffOutcome.CustomizedValueCanUpdate: { + if (conflict === ThreeWayDiffConflict.SOLVABLE) { + return [ + hasResolvedValueDifferentFromSuggested + ? VersionsPickerOptionEnum.MyChanges + : VersionsPickerOptionEnum.Merged, + VersionsPickerOptionEnum.UpdateFromElastic, + VersionsPickerOptionEnum.MyOriginalChanges, + ]; + } + + if (conflict === ThreeWayDiffConflict.NON_SOLVABLE) { + const options = [ + VersionsPickerOptionEnum.MyChanges, + VersionsPickerOptionEnum.UpdateFromElastic, + ]; + + if (hasResolvedValueDifferentFromSuggested) { + options.push(VersionsPickerOptionEnum.MyOriginalChanges); + } + + return options; + } + } + case ThreeWayDiffOutcome.MissingBaseCanUpdate: { + const options = []; + + if (hasResolvedValueDifferentFromSuggested) { + options.push(VersionsPickerOptionEnum.MyChanges); + } + options.push(VersionsPickerOptionEnum.UpdateFromElastic); + + return options; + } + default: + return []; + } +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/constants.ts index 04d38ed10dce2..9c6292451b47b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/constants.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/constants.ts @@ -5,54 +5,9 @@ * 2.0. */ -import type { EuiSelectOption } from '@elastic/eui'; -import * as i18n from './translations'; - export enum Version { Base = 'base', Current = 'current', Target = 'target', Final = 'final', } - -export enum SelectedVersions { - BaseTarget = 'base_target', - BaseCurrent = 'base_current', - BaseFinal = 'base_final', - CurrentTarget = 'current_target', - CurrentFinal = 'current_final', - TargetFinal = 'target_final', -} - -export const CURRENT_OPTIONS: EuiSelectOption[] = [ - { - value: SelectedVersions.CurrentFinal, - text: i18n.VERSION1_VS_VERSION2(i18n.CURRENT_VERSION, i18n.FINAL_VERSION), - }, - { - value: SelectedVersions.CurrentTarget, - text: i18n.VERSION1_VS_VERSION2(i18n.CURRENT_VERSION, i18n.TARGET_VERSION), - }, -]; - -export const TARGET_OPTIONS: EuiSelectOption[] = [ - { - value: SelectedVersions.TargetFinal, - text: i18n.VERSION1_VS_VERSION2(i18n.TARGET_VERSION, i18n.FINAL_VERSION), - }, -]; - -export const BASE_OPTIONS: EuiSelectOption[] = [ - { - value: SelectedVersions.BaseFinal, - text: i18n.VERSION1_VS_VERSION2(i18n.BASE_VERSION, i18n.FINAL_VERSION), - }, - { - value: SelectedVersions.BaseTarget, - text: i18n.VERSION1_VS_VERSION2(i18n.BASE_VERSION, i18n.TARGET_VERSION), - }, - { - value: SelectedVersions.BaseCurrent, - text: i18n.VERSION1_VS_VERSION2(i18n.BASE_VERSION, i18n.CURRENT_VERSION), - }, -]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/versions_picker.stories.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/versions_picker.stories.tsx index c9193e2c358ad..75486d4176c82 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/versions_picker.stories.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/versions_picker.stories.tsx @@ -6,42 +6,23 @@ */ import React, { useState } from 'react'; -import type { Story } from '@storybook/react'; -import { VersionsPicker } from './versions_picker'; -import { SelectedVersions } from './constants'; +import { VersionsPicker, VersionsPickerOptionEnum } from './versions_picker'; export default { component: VersionsPicker, title: 'Rule Management/Prebuilt Rules/Upgrade Flyout/ThreeWayDiff/VersionsPicker', - argTypes: { - hasBaseVersion: { - control: 'boolean', - description: 'Indicates whether the base version of a field is available', - defaultValue: true, - }, - }, }; -const Template: Story<{ hasBaseVersion: boolean }> = (args) => { - const [selectedVersions, setSelectedVersions] = useState( - SelectedVersions.CurrentFinal - ); +export const Default = () => { + const options = [VersionsPickerOptionEnum.MyChanges, VersionsPickerOptionEnum.UpdateFromElastic]; + const [selectedOption, setSelectedOption] = useState(options[0]); return ( ); }; - -export const Default = Template.bind({}); -Default.args = { - hasBaseVersion: true, -}; - -export const NoBaseVersion = Template.bind({}); -NoBaseVersion.args = { - hasBaseVersion: false, -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/versions_picker.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/versions_picker.tsx index 4710667e0b315..6fb33113bc36d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/versions_picker.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/comparison_side/versions_picker/versions_picker.tsx @@ -5,32 +5,48 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback } from 'react'; import { css } from '@emotion/css'; import { EuiSelect } from '@elastic/eui'; -import type { EuiSelectOption } from '@elastic/eui'; -import { BASE_OPTIONS, CURRENT_OPTIONS, TARGET_OPTIONS, SelectedVersions } from './constants'; +import { getOptionDetails } from '../utils'; import * as i18n from './translations'; +export enum VersionsPickerOptionEnum { + MyChanges = 'MY_CHANGES', + MyOriginalChanges = 'MY_ORIGINAL_CHANGES', + UpdateFromElastic = 'UPDATE_FROM_ELASTIC', + Merged = 'MERGED', +} + interface VersionsPickerProps { - hasBaseVersion: boolean; - selectedVersions: SelectedVersions; - onChange: (pickedVersions: SelectedVersions) => void; + options: VersionsPickerOptionEnum[]; + selectedOption: VersionsPickerOptionEnum; + onChange: (selectedOption: VersionsPickerOptionEnum) => void; + hasResolvedValueDifferentFromSuggested: boolean; } export function VersionsPicker({ - hasBaseVersion, - selectedVersions = SelectedVersions.CurrentFinal, + options, + selectedOption, onChange, + hasResolvedValueDifferentFromSuggested, }: VersionsPickerProps) { - const options: EuiSelectOption[] = useMemo( - () => [...CURRENT_OPTIONS, ...TARGET_OPTIONS, ...(hasBaseVersion ? BASE_OPTIONS : [])], - [hasBaseVersion] - ); + const euiSelectOptions = options.map((option) => { + const { title: displayName, description: explanation } = getOptionDetails( + option, + hasResolvedValueDifferentFromSuggested + ); + + return { + value: option, + text: displayName, + title: explanation, + }; + }); const handleChange = useCallback( (changeEvent: React.ChangeEvent) => { - onChange(changeEvent.target.value as SelectedVersions); + onChange(changeEvent.target.value as VersionsPickerOptionEnum); }, [onChange] ); @@ -38,8 +54,8 @@ export function VersionsPicker({ return ( @@ -49,5 +65,5 @@ export function VersionsPicker({ const VERSIONS_PICKER_STYLES = css` // Set min-width a bit wider than default // to make English text in narrow screens readable - min-width: 220px; + min-width: 300px; `; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/field_final_side/components/field_final_side_help_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/field_final_side/components/field_final_side_help_info.tsx index e3e0b38da7d63..ef8cfc820535d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/field_final_side/components/field_final_side_help_info.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/field_final_side/components/field_final_side_help_info.tsx @@ -9,6 +9,7 @@ import React from 'react'; import useToggle from 'react-use/lib/useToggle'; import { EuiPopover, EuiText, EuiButtonIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import * as i18n from '../../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/translations'; /** * Theme doesn't expose width variables. Using provided size variables will require @@ -34,8 +35,11 @@ export function FieldFinalSideHelpInfo(): JSX.Element { {i18n.UPDATE_BUTTON_LABEL}, + }} /> diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_context.tsx index 6f8a6033de406..fd2811bdcce91 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_context.tsx @@ -6,6 +6,7 @@ */ import React, { createContext, useContext, useMemo } from 'react'; +import { isEqual } from 'lodash'; import { useBoolean } from '@kbn/react-hooks'; import { assertUnreachable } from '../../../../../../../common/utility_types'; import { @@ -39,8 +40,14 @@ interface FieldUpgradeContextType { * Whether the field has an unresolved conflict. This state is derived from `fieldUpgradeState`. */ hasConflict: boolean; + /** + * Whether field value is different from Elastic's suggestion. + * It's true only if user has made changes to the suggested field value. + */ + hasResolvedValueDifferentFromSuggested: boolean; /** * Whether the field was changed after prebuilt rule installation, i.e. customized + * It's true only if user has made changes to the suggested field value. */ isCustomized: boolean; /** @@ -97,6 +104,8 @@ export function FieldUpgradeContextProvider({ invariant(fieldDiff, `Field diff is not found for ${fieldName}.`); + const finalDiffableRule = calcFinalDiffableRule(ruleUpgradeState); + const contextValue: FieldUpgradeContextType = useMemo( () => ({ fieldName, @@ -104,9 +113,17 @@ export function FieldUpgradeContextProvider({ hasConflict: fieldUpgradeState === FieldUpgradeStateEnum.SolvableConflict || fieldUpgradeState === FieldUpgradeStateEnum.NonSolvableConflict, + /* + Initially, we prefill the resolved value with the merged version. + If the current resolved value differs from the merged version, it indicates that the user has modified the suggestion. + */ + hasResolvedValueDifferentFromSuggested: !isEqual( + fieldDiff.merged_version, + finalDiffableRule[fieldName] + ), isCustomized: calcIsCustomized(fieldDiff), fieldDiff, - finalDiffableRule: calcFinalDiffableRule(ruleUpgradeState), + finalDiffableRule, rightSideMode: editing ? FieldFinalSideMode.Edit : FieldFinalSideMode.Readonly, setRuleFieldResolvedValue, setReadOnlyMode, @@ -116,7 +133,7 @@ export function FieldUpgradeContextProvider({ fieldName, fieldUpgradeState, fieldDiff, - ruleUpgradeState, + finalDiffableRule, editing, setRuleFieldResolvedValue, setReadOnlyMode, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_state_info/field_upgrade_state_info.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_state_info/field_upgrade_state_info.tsx index a85cf96cc50d2..ed2f7050a2c90 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_state_info/field_upgrade_state_info.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_state_info/field_upgrade_state_info.tsx @@ -28,6 +28,13 @@ export function FieldUpgradeStateInfo({ state }: FieldUpgradeStateInfoProps): JS description: i18n.NO_UPDATE_DESCRIPTION, }; + case FieldUpgradeStateEnum.SameUpdate: + return { + color: 'success', + title: i18n.SAME_UPDATE, + description: i18n.SAME_UPDATE_DESCRIPTION, + }; + case FieldUpgradeStateEnum.NoConflict: return { color: 'success', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_state_info/translations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_state_info/translations.tsx index c3115c6ce0925..0b67f590d3f64 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_state_info/translations.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/rule_upgrade/field_upgrade_state_info/translations.tsx @@ -22,6 +22,21 @@ export const NO_UPDATE_DESCRIPTION = i18n.translate( } ); +export const SAME_UPDATE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.sameUpdate', + { + defaultMessage: 'Matching update', + } +); + +export const SAME_UPDATE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.sameUpdateDescription', + { + defaultMessage: + 'The field was modified after rule installation, and your changes are the same as the update from Elastic.', + } +); + export const NO_CONFLICT = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.upgradeRules.fieldUpgradeState.noConflict', { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/field_upgrade_state_enum.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/field_upgrade_state_enum.ts index 0fd522403edc6..241bb1221a6f3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/field_upgrade_state_enum.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/model/prebuilt_rule_upgrade/field_upgrade_state_enum.ts @@ -7,6 +7,7 @@ export enum FieldUpgradeStateEnum { NoUpdate = 'NO_UPDATE', + SameUpdate = 'SAME_UPDATE', NoConflict = 'NO_CONFLICT', Accepted = 'ACCEPTED', SolvableConflict = 'SOLVABLE_CONFLICT', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts index 81d64b1b39945..3412947426301 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state.ts @@ -20,6 +20,7 @@ import { ThreeWayDiffConflict, type RuleSignatureId, NON_UPGRADEABLE_DIFFABLE_FIELDS, + ThreeWayDiffOutcome, } from '../../../../../../common/api/detection_engine'; import { assertUnreachable } from '../../../../../../common/utility_types'; @@ -104,11 +105,18 @@ function calcFieldsState( switch (fieldDiff.conflict) { case ThreeWayDiffConflict.NONE: - fieldsState[fieldName] = { - state: fieldDiff.has_update - ? FieldUpgradeStateEnum.NoConflict - : FieldUpgradeStateEnum.NoUpdate, - }; + if (fieldDiff.has_update) { + fieldsState[fieldName] = { + state: FieldUpgradeStateEnum.NoConflict, + }; + } else { + fieldsState[fieldName] = { + state: + fieldDiff.diff_outcome === ThreeWayDiffOutcome.CustomizedValueSameUpdate + ? FieldUpgradeStateEnum.SameUpdate + : FieldUpgradeStateEnum.NoUpdate, + }; + } break; case ThreeWayDiffConflict.SOLVABLE: