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: ( <>
), @@ -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: