Skip to content

Commit

Permalink
[Security Solution] Integrate state and components for Prebuilt Rule …
Browse files Browse the repository at this point in the history
…Update Workflow (elastic#193531)

**Epic:** elastic#174168
**Addresses:** elastic#171520

## Summary

This PR introduces a new `Update` tab allowing users to resolve rule upgrade conflicts. It's a result of combination of read-only components implemented in elastic#193261 and rule upgrade state implemented in elastic#191721.

## Details

The goal of this PR is to provide intermediate integration between rule upgrade state ([PR](elastic#191721)) and components displaying the diff and read-only state ([PR](elastic#193261)). It will facilitate further development of rule field editable components and streamline rule upgrade functionality developing.

## How to test?

The functionality is hidden under `prebuiltRulesCustomizationEnabled` feature flag. Add the following to your Kibana config

```yaml
xpack.securitySolution.enableExperimental:
  - prebuiltRulesCustomizationEnabled
```

When the above feature flag enabled the new `Update` tab is displayed instead of the old one.

## Screenshots

Suggested components design 
![image](https://github.com/user-attachments/assets/b5aaf571-286a-4595-9bd4-fdaf9a423b03)

New `Update` tab
<img width="1718" alt="image" src="https://github.com/user-attachments/assets/28aa6bb3-f805-4109-a808-d67e58c7c5b8">
  • Loading branch information
maximpn authored Sep 27, 2024
1 parent a9fa11e commit 878ba13
Show file tree
Hide file tree
Showing 21 changed files with 538 additions and 141 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './split_accordion';
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { EuiAccordion, EuiSplitPanel, useEuiTheme, useGeneratedHtmlId } from '@elastic/eui';
import { css } from '@emotion/css';
import type { PropsWithChildren } from 'react';

interface SplitAccordionProps {
header: React.ReactNode;
initialIsOpen?: boolean;
'data-test-subj'?: string;
}

export const SplitAccordion = ({
header,
initialIsOpen,
'data-test-subj': dataTestSubj,
children,
}: PropsWithChildren<SplitAccordionProps>) => {
const accordionId = useGeneratedHtmlId();
const { euiTheme } = useEuiTheme();

return (
<EuiSplitPanel.Outer data-test-subj={`${dataTestSubj}Wrapper`} hasBorder>
<EuiAccordion
id={accordionId}
initialIsOpen={initialIsOpen}
css={css`
.euiAccordion__triggerWrapper {
background: ${euiTheme.colors.lightestShade};
padding: ${euiTheme.size.m};
}
`}
buttonContent={header}
>
<EuiSplitPanel.Inner data-test-subj={`${dataTestSubj}Content`} color="transparent">
{children}
</EuiSplitPanel.Inner>
</EuiAccordion>
</EuiSplitPanel.Outer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import { EuiFlexGroup, EuiHorizontalRule, EuiTitle } from '@elastic/eui';
import { camelCase, startCase } from 'lodash';
import React from 'react';
import { SplitAccordion } from '../../../../../common/components/split_accordion';
import { DiffView } from '../json_diff/diff_view';
import { RuleDiffPanelWrapper } from './panel_wrapper';
import type { FormattedFieldDiff, FieldDiff } from '../../../model/rule_details/rule_field_diff';
import { fieldToDisplayNameMap } from './translations';

Expand Down Expand Up @@ -46,21 +46,31 @@ export const FieldGroupDiffComponent = ({
fieldsGroupName,
}: FieldDiffComponentProps) => {
const { fieldDiffs, shouldShowSubtitles } = ruleDiffs;

return (
<RuleDiffPanelWrapper fieldName={fieldsGroupName}>
<SplitAccordion
header={
<EuiTitle data-test-subj="ruleUpgradePerFieldDiffLabel" size="xs">
<h5>{fieldToDisplayNameMap[fieldsGroupName] ?? startCase(camelCase(fieldsGroupName))}</h5>
</EuiTitle>
}
initialIsOpen={true}
data-test-subj="ruleUpgradePerFieldDiff"
>
{fieldDiffs.map(({ currentVersion, targetVersion, fieldName: specificFieldName }, index) => {
const shouldShowSeparator = index !== fieldDiffs.length - 1;
const isLast = index === fieldDiffs.length - 1;

return (
<SubFieldComponent
key={specificFieldName}
shouldShowSeparator={shouldShowSeparator}
shouldShowSeparator={!isLast}
shouldShowSubtitles={shouldShowSubtitles}
currentVersion={currentVersion}
targetVersion={targetVersion}
fieldName={specificFieldName}
/>
);
})}
</RuleDiffPanelWrapper>
</SplitAccordion>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@

export * from './field_diff';
export * from './header_bar';
export * from './panel_wrapper';
export * from './rule_diff_section';

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*/

import React, { useState } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { VersionsPicker } from '../versions_picker/versions_picker';
import type { Version } from '../versions_picker/constants';
import { SelectedVersions } from '../versions_picker/constants';
Expand All @@ -17,6 +16,7 @@ import type {
} from '../../../../../../../common/api/detection_engine';
import { getSubfieldChanges } from './get_subfield_changes';
import { SubfieldChanges } from './subfield_changes';
import { SideHeader } from '../components/side_header';

interface ComparisonSideProps<FieldName extends keyof DiffableAllFields> {
fieldName: FieldName;
Expand All @@ -42,12 +42,13 @@ export function ComparisonSide<FieldName extends keyof DiffableAllFields>({

return (
<>
<VersionsPicker
hasBaseVersion={fieldThreeWayDiff.has_base_version}
selectedVersions={selectedVersions}
onChange={setSelectedVersions}
/>
<EuiSpacer size="m" />
<SideHeader>
<VersionsPicker
hasBaseVersion={fieldThreeWayDiff.has_base_version}
selectedVersions={selectedVersions}
onChange={setSelectedVersions}
/>
</SideHeader>
<SubfieldChanges fieldName={fieldName} subfieldChanges={subfieldChanges} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function pickFieldValueForVersion<FieldName extends keyof DiffableAllFiel
resolvedValue?: DiffableAllFields[FieldName]
): DiffableAllFields[FieldName] | undefined {
if (version === Version.Final) {
return resolvedValue ?? fieldThreeWayDiff.merged_version;
return resolvedValue;
}

const versionFieldToPick = `${version}_version` as const;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
import { SplitAccordion } from '../../../../../../common/components/split_accordion';
import type {
DiffableAllFields,
DiffableRule,
RuleFieldsDiff,
ThreeWayDiff,
} from '../../../../../../../common/api/detection_engine';
import { ThreeWayDiffConflict } from '../../../../../../../common/api/detection_engine';
import { ComparisonSide } from '../comparison_side/comparison_side';
import { FinalSide } from '../final_side/final_side';
import { FieldUpgradeConflictsResolverHeader } from './field_upgrade_conflicts_resolver_header';

interface FieldUpgradeConflictsResolverProps<FieldName extends keyof RuleFieldsDiff> {
fieldName: FieldName;
fieldThreeWayDiff: RuleFieldsDiff[FieldName];
finalDiffableRule: DiffableRule;
}

export function FieldUpgradeConflictsResolver<FieldName extends keyof RuleFieldsDiff>({
fieldName,
fieldThreeWayDiff,
finalDiffableRule,
}: FieldUpgradeConflictsResolverProps<FieldName>): JSX.Element {
const { euiTheme } = useEuiTheme();
const hasConflict = fieldThreeWayDiff.conflict !== ThreeWayDiffConflict.NONE;

return (
<>
<SplitAccordion
header={<FieldUpgradeConflictsResolverHeader fieldName={fieldName} />}
initialIsOpen={hasConflict}
data-test-subj="ruleUpgradePerFieldDiff"
>
<EuiFlexGroup gutterSize="s" alignItems="flexStart">
<EuiFlexItem grow={1}>
<ComparisonSide
fieldName={fieldName}
fieldThreeWayDiff={fieldThreeWayDiff as ThreeWayDiff<DiffableAllFields[FieldName]>}
resolvedValue={finalDiffableRule[fieldName] as DiffableAllFields[FieldName]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={0}
css={css`
align-self: stretch;
border-right: ${euiTheme.border.thin};
`}
/>
<EuiFlexItem grow={1}>
<FinalSide fieldName={fieldName} finalDiffableRule={finalDiffableRule} />
</EuiFlexItem>
</EuiFlexGroup>
</SplitAccordion>
<EuiSpacer size="s" />
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { camelCase, startCase } from 'lodash';
import { EuiTitle } from '@elastic/eui';
import { fieldToDisplayNameMap } from '../../diff_components/translations';

interface FieldUpgradeConflictsResolverHeaderProps {
fieldName: string;
}

export function FieldUpgradeConflictsResolverHeader({
fieldName,
}: FieldUpgradeConflictsResolverHeaderProps): JSX.Element {
return (
<EuiTitle data-test-subj="ruleUpgradeFieldDiffLabel" size="xs">
<h5>{fieldToDisplayNameMap[fieldName] ?? startCase(camelCase(fieldName))}</h5>
</EuiTitle>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import type {
RuleUpgradeState,
SetRuleFieldResolvedValueFn,
} from '../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state';
import { FieldUpgradeConflictsResolver } from './field_upgrade_conflicts_resolver';

interface RuleUpgradeConflictsResolverProps {
ruleUpgradeState: RuleUpgradeState;
setRuleFieldResolvedValue: SetRuleFieldResolvedValueFn;
}

export function RuleUpgradeConflictsResolver({
ruleUpgradeState,
setRuleFieldResolvedValue,
}: RuleUpgradeConflictsResolverProps): JSX.Element {
const fieldDiffEntries = Object.entries(ruleUpgradeState.diff.fields) as Array<
[
keyof typeof ruleUpgradeState.diff.fields,
Required<typeof ruleUpgradeState.diff.fields>[keyof typeof ruleUpgradeState.diff.fields]
]
>;
const fields = fieldDiffEntries.map(([fieldName, fieldDiff]) => (
<FieldUpgradeConflictsResolver
key={fieldName}
fieldName={fieldName}
fieldThreeWayDiff={fieldDiff}
finalDiffableRule={ruleUpgradeState.finalRule}
/>
));

return <>{fields}</>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import type { RuleUpgradeState } from '../../../../../rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/use_prebuilt_rules_upgrade_state';
import {
UtilityBar,
UtilityBarGroup,
UtilityBarSection,
UtilityBarText,
} from '../../../../../../common/components/utility_bar';
import * as i18n from './translations';

interface UpgradeInfoBarProps {
ruleUpgradeState: RuleUpgradeState;
}

export function RuleUpgradeInfoBar({ ruleUpgradeState }: UpgradeInfoBarProps): JSX.Element {
const numOfFieldsWithUpdates = ruleUpgradeState.diff.num_fields_with_updates;
const numOfConflicts = ruleUpgradeState.diff.num_fields_with_conflicts;

return (
<UtilityBar>
<UtilityBarSection>
<UtilityBarGroup>
<UtilityBarText dataTestSubj="showingRules">
{i18n.NUM_OF_FIELDS_WITH_UPDATES(numOfFieldsWithUpdates)}
</UtilityBarText>
</UtilityBarGroup>
<UtilityBarGroup>
<UtilityBarText dataTestSubj="showingRules">
{i18n.NUM_OF_CONFLICTS(numOfConflicts)}
</UtilityBarText>
</UtilityBarGroup>
</UtilityBarSection>
<UtilityBarSection>
<UtilityBarGroup>
<i18n.RuleUpgradeHelper />
</UtilityBarGroup>
</UtilityBarSection>
</UtilityBar>
);
}
Loading

0 comments on commit 878ba13

Please sign in to comment.