[Security Solution] Reduce dropdown options and improve tooltip texts in Rule Upgrade flyout (#203222)

**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
<img width="922" alt="Scherm­afbeelding 2024-12-11 om 11 54 48"
src="https://github.com/user-attachments/assets/124e76a1-99dc-48d8-be54-f6c8f2079451">

<img width="640" alt="Scherm­afbeelding 2024-12-11 om 11 55 32"
src="https://github.com/user-attachments/assets/45655dd2-6503-46b7-b28b-0df7bf0e6fa3">

<img width="433" alt="Scherm­afbeelding 2024-12-11 om 11 55 58"
src="https://github.com/user-attachments/assets/d845ff52-4678-4245-8bdd-b9957f0c1d13">


Work started on 06-Dec-2024.

---------

Co-authored-by: Maxim Palenov <maxim.palenov@elastic.co>
This commit is contained in:
Nikita Indik 2024-12-14 12:40:45 +01:00 committed by GitHub
parent 9a8ed0d135
commit 90e35a04bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 366 additions and 166 deletions

View file

@ -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 = (
<EuiButtonIcon
iconType="questionInCircle"
@ -43,25 +48,20 @@ export function ComparisonSideHelpInfo(): JSX.Element {
<EuiText style={{ width: POPOVER_WIDTH }} size="s">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.upgradeRules.comparisonSide.upgradeHelpText"
defaultMessage="{title} shows field's JSON diff between prebuilt rule field versions affecting the rule update process. {versions}"
defaultMessage="The {title} lets you compare the values of a field across different versions of a rule: {versions} Differences are shown as JSON, with red lines showing what was removed, green lines showing additions, and bold text highlighting changes. Use {title} to review and understand changes across versions."
values={{
title: <strong>{TITLE}</strong>,
versions: (
<>
<br />
<ul>
<li>
<strong>{BASE_VERSION}</strong> {'-'} {BASE_VERSION_EXPLANATION}
</li>
<li>
<strong>{CURRENT_VERSION}</strong> {'-'} {CURRENT_VERSION_EXPLANATION}
</li>
<li>
<strong>{TARGET_VERSION}</strong> {'-'} {TARGET_VERSION_EXPLANATION}
</li>
<li>
<strong>{FINAL_VERSION}</strong> {'-'} {FINAL_VERSION_EXPLANATION}
</li>
{optionsWithDescriptions.map(
({ title: displayName, description: explanation }) => (
<li>
<strong>{displayName}</strong> {'-'} {explanation}
</li>
)
)}
</ul>
</>
),
@ -71,35 +71,3 @@ export function ComparisonSideHelpInfo(): JSX.Element {
</EuiPopover>
);
}
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 = (
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.upgradeRules.currentVersionExplanation"
defaultMessage="current version including modification made after prebuilt rule installation. With lack of modifications it matches with {base}."
values={{
base: <strong>{BASE_VERSION}</strong>,
}}
/>
);
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.',
}
);

View file

@ -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>(
SelectedVersions.CurrentFinal
const options = getComparisonOptionsForDiffOutcome(
fieldDiff.diff_outcome,
fieldDiff.conflict,
hasResolvedValueDifferentFromSuggested
);
const [selectedOption, setSelectedOption] = useState<VersionsPickerOptionEnum>(options[0]);
const [oldVersionType, newVersionType] = getVersionsForComparison(
selectedOption,
fieldDiff.has_base_version
);
const [oldVersionType, newVersionType] = selectedVersions.split('_') as [Version, 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 (
<>
<FieldUpgradeSideHeader>
@ -42,15 +63,16 @@ export function FieldComparisonSide(): JSX.Element {
<EuiTitle size="xxs">
<h3>
{i18n.TITLE}
<ComparisonSideHelpInfo />
<ComparisonSideHelpInfo options={options} />
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<VersionsPicker
hasBaseVersion={fieldDiff.has_base_version}
selectedVersions={selectedVersions}
onChange={setSelectedVersions}
options={options}
selectedOption={selectedOption}
onChange={setSelectedOption}
hasResolvedValueDifferentFromSuggested={hasResolvedValueDifferentFromSuggested}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -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 Elastics 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 Elastics',
}
);
export const MERGED_CHANGES_EXPLANATION = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.versions.mergedChangesExplanation',
{
defaultMessage: 'view an update suggestion that combines your changes with Elastics',
}
);
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. Doesnt include changes made in the {finalUpdateSectionLabel} section.`,
values: {
finalUpdateSectionLabel: FINAL_UPDATE,
},
}
);

View file

@ -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 [];
}
};

View file

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

View file

@ -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>(
SelectedVersions.CurrentFinal
);
export const Default = () => {
const options = [VersionsPickerOptionEnum.MyChanges, VersionsPickerOptionEnum.UpdateFromElastic];
const [selectedOption, setSelectedOption] = useState<VersionsPickerOptionEnum>(options[0]);
return (
<VersionsPicker
hasBaseVersion={args.hasBaseVersion}
selectedVersions={selectedVersions}
onChange={setSelectedVersions}
options={options}
selectedOption={selectedOption}
onChange={setSelectedOption}
hasResolvedValueDifferentFromSuggested={false}
/>
);
};
export const Default = Template.bind({});
Default.args = {
hasBaseVersion: true,
};
export const NoBaseVersion = Template.bind({});
NoBaseVersion.args = {
hasBaseVersion: false,
};

View file

@ -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<HTMLSelectElement>) => {
onChange(changeEvent.target.value as SelectedVersions);
onChange(changeEvent.target.value as VersionsPickerOptionEnum);
},
[onChange]
);
@ -38,8 +54,8 @@ export function VersionsPicker({
return (
<EuiSelect
className={VERSIONS_PICKER_STYLES}
options={options}
value={selectedVersions}
options={euiSelectOptions}
value={selectedOption}
onChange={handleChange}
aria-label={i18n.VERSION_PICKER_ARIA_LABEL}
/>
@ -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;
`;

View file

@ -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 {
<EuiPopover button={button} isOpen={isPopoverOpen} closePopover={togglePopover}>
<EuiText style={{ width: POPOVER_WIDTH }} size="s">
<FormattedMessage
id="xpack.securitySolution.detectionEngine.rules.upgradeRules.finalSide.upgradeHelpText"
defaultMessage="Choose field values used in the upgraded rule. "
id="xpack.securitySolution.detectionEngine.rules.upgradeRules.upgradeHelpText"
defaultMessage="The Final Update section lets you preview and edit the final value of a field. This is the value the rule will have after you click {updateButtonLabel}."
values={{
updateButtonLabel: <strong>{i18n.UPDATE_BUTTON_LABEL}</strong>,
}}
/>
</EuiText>
</EuiPopover>

View file

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

View file

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

View file

@ -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',
{

View file

@ -7,6 +7,7 @@
export enum FieldUpgradeStateEnum {
NoUpdate = 'NO_UPDATE',
SameUpdate = 'SAME_UPDATE',
NoConflict = 'NO_CONFLICT',
Accepted = 'ACCEPTED',
SolvableConflict = 'SOLVABLE_CONFLICT',

View file

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