Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Add ComparisonSide component #189384

Merged
merged 9 commits into from
Aug 8, 2024

Conversation

nikitaindik
Copy link
Contributor

@nikitaindik nikitaindik commented Jul 29, 2024

Partially addresses: #171520

Summary

This PR adds the ComparisonSide component for the ThreeWayDiff UI (see it on the Miro diagram).

ComparisonSide lets the user compare field values from the two selected rule versions. It will be displayed on the left side of the upgrade flyout.

You can view and test it in Storybook by running yarn storybook security_solution in the root Kibana dir. Go to http://localhost:9001 once the Storybook is up and running.

comparison_side_in_storybook.mov

Also updated react-diff-view to the latest version (3.2.0 -> 3.2.1)

@nikitaindik nikitaindik added release_note:skip Skip the PR/issue when compiling release notes Team:Detections and Resp Security Detection Response Team Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. Team:Detection Rule Management Security Detection Rule Management Team Feature:Prebuilt Detection Rules Security Solution Prebuilt Detection Rules v8.16.0 labels Jul 29, 2024
@nikitaindik nikitaindik self-assigned this Jul 29, 2024
@nikitaindik
Copy link
Contributor Author

/ci

1 similar comment
@nikitaindik
Copy link
Contributor Author

/ci

@nikitaindik nikitaindik marked this pull request as ready for review July 30, 2024 08:22
@nikitaindik nikitaindik requested a review from a team as a code owner July 30, 2024 08:22
@nikitaindik nikitaindik requested a review from maximpn July 30, 2024 08:22
@elasticmachine
Copy link
Contributor

Pinging @elastic/security-detections-response (Team:Detections and Resp)

@elasticmachine
Copy link
Contributor

Pinging @elastic/security-solution (Team: SecuritySolution)

@elasticmachine
Copy link
Contributor

Pinging @elastic/security-detection-rule-management (Team:Detection Rule Management)

@nikitaindik
Copy link
Contributor Author

/ci

@banderror banderror requested a review from dplumlee July 31, 2024 17:59
Copy link
Contributor

@maximpn maximpn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nikitaindik thanks for you effort in implementing ComparisonSide component. It looks nice and works well. I tested in storybooks.

There are no tests for the added component here. Do you plan to add them in a separate PR?

I left comments regarding subfield changes implementation here. Feel free to reach me for additional clarification.

@@ -1148,7 +1148,7 @@
"re2js": "0.4.1",
"react": "^17.0.2",
"react-ace": "^7.0.5",
"react-diff-view": "^3.2.0",
"react-diff-view": "^3.2.1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious if that update brings some fixes required for the feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It fixes a minor bug with displaying newlines at the end of the diff: https://github.com/otakustay/react-diff-view/blob/master/CHANGELOG.md#321-2024-02-18 Not a big deal for us, but I decide to update anyways.

* @returns {string} A unified diff string representing the changes.
*/
const convertChangesToUnifiedDiffString = (changes: Change[]) => {
const unifiedDiff: string = unidiff.formatLines(changes, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious why do you use explicit typing for a constant but omit function's return type making it inferred?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Underrefactored a bit. Adding an explicit return type is a good idea! Added.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: TS can infer string type for unifiedDiff

Suggested change
const unifiedDiff: string = unidiff.formatLines(changes, {
const unifiedDiff = unidiff.formatLines(changes, {

} from '../../../../../../../common/api/detection_engine';
import type { SubfieldChange } from './types';

export const sortAndStringifyJson = (fieldValue: unknown): string => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const sortAndStringifyJson = (fieldValue: unknown): string => {
export const stringifyToStableJson = (fieldValue: unknown): string => {

As a general rule of thumb having And in function name is bad practice meaning it either has mixed responsibility and should be split or the naming isn't good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed this offline. Renamed to stringifyToSortedJson.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered creating a folder with multiple files + an index file instead of creation ~500 lines file?

Btw name get_subfield_changes_for_field looks like overkill it looks like get_subfield_changes is enough. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have thought about it but wasn't sure if splitting it was worth the effort. Now since you brought that up I have refactored it into multiple files. 👍

diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Target,
has_update: true,
conflict: ThreeWayDiffConflict.NONE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Shouldn't states be sorted from simple to more complex states? First states with ThreeWayDiffConflict.NONE are placed having ThreeWayDiffConflict.SOLVABLE and ThreeWayDiffConflict.NON_SOLVABLE the last.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually my main idea was to just see / show how different field types (single / multi-line, numbers, arrays) are rendered. I didn't pay much attention to conflict since it doesn't directly affect this component.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input data should be accurate. You can implement some function to generate input data to mitigate such situations. The problem here is that some can use storybook's data as a reference and might be confused by an unexpected conflict value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input data should be accurate.

I agree. And I think it is accurate in this case.

conflict is NONE, because it's an AAB situation, so the target value is taken and there's no conflict. Isn't it?

export const getSubfieldChangesForDataSource = (
oldFieldValue?: RuleDataSource,
newFieldValue?: RuleDataSource
): SubfieldChange[] => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Shouldn't Readonly<SubfieldChange[]> be used everywhere in this PR instead of SubfieldChange[]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made all SubfieldChange properties readonly. But we can't easily make the array of SubfieldChanges readonly because we push into an array as we build it.

return stringify(fieldValue, { space: 2 });
};

export const getSubfieldChangesForDataSource = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to invoke this function with both arguments undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed this offline. Only one arg can be undefined but describing it with TS is difficult in this situation.

Comment on lines 54 to 55
oldFieldValue as RuleDataSource | undefined,
newFieldValue as RuleDataSource | undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using as should be sparse and limited to only necessary cases. In this case TS is force to think oldFieldValue is RuleDataSource type while it can be a different type in runtime.

To get rid of as in this particular case either getSubfieldChangesForDataSource() input types should be relaxed or runtime check should be added.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Def agree this isn't the best approach. I couldn't get it to work quickly without typecasting. I would appreciate it if we could pair and improve the types here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed this offline. It turned out it's not possible to avoid typecasting since TS doesn't support narrowing union types. Added an explainer comment in code. We may want to discuss the type situation at the upcoming Tech Time.

}

export function SubfieldHeader({ fieldName, subfieldName }: SubfieldHeaderProps) {
if (!shouldDisplaySubfieldLabelsForField(fieldName)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SubfieldHeader as a leaf component shouldn't know about implementation details of the other components. This logic should be a few levels up in Subfield component. It's also good to leave a comment why header isn't always shown.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


function shouldDisplaySubfieldLabelsForField(fieldName: string): boolean {
return [
'data_source',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded field names make it harder to maintain this functionality later on when we decide to remove or rename some fields. The good way is to specify a union of rule field names.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Updated.

@nikitaindik nikitaindik marked this pull request as draft August 6, 2024 17:04
@nikitaindik nikitaindik marked this pull request as ready for review August 6, 2024 17:06
@nikitaindik
Copy link
Contributor Author

/ci

@nikitaindik
Copy link
Contributor Author

Thanks for your feedback @maximpn! I have addressed your comments. Feel free to take another look!

As for the tests, I plan to add them later once the base functionality is completed and the test plan is ready.

Copy link
Contributor

@dplumlee dplumlee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Component looks great @nikitaindik! This was my first time diving deep with storybooks, very cool. Left a few nits related to overall code structure but I think the refactored code looks good!

Comment on lines +39 to +49
export const stringifyToSortedJson = (fieldValue: unknown): string => {
if (fieldValue === undefined) {
return '';
}

if (typeof fieldValue === 'string') {
return fieldValue;
}

return stringify(fieldValue, { space: 2 });
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we define an identical function for the per-field diff here, might be worth abstracting both of them out

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's actually the same function. I copy-pasted it from the per-field diff. I figured that copy-pasting is fine since we are going to remove the per-field diff code at some point once this new tab is ready. But since the function is very generic, perhaps it's a good idea to extract it away.

Any recommendations for a good place to put it? I couldn't find a suitable utils file. If you have a recommendation, I can move it there in the next PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exactly the same thing but we could put it in this helpers file: x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/helpers.ts? I probably should've put it there initially as we have another similar function in rule_diff_tab.tsx that you originally added.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Will add to my todo for the next PR.

Comment on lines 13 to 25
const FIELDS_WITH_SUBFIELDS: Array<keyof DiffableAllFields> = [
'data_source',
'kql_query',
'eql_query',
'esql_query',
'threat_query',
'rule_schedule',
'rule_name_override',
'timestamp_override',
'timeline_template',
'building_block',
'threshold',
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this could be put in a constants.ts file in this folder or in the above folder, we'll likely have other stuff to add to it or reference these at some point in the future (tests, etc)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Copy link
Contributor

@maximpn maximpn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nikitaindik thank you for addressing my comments 👍

I left a few more but these don't look critical. @dplumlee mentioned that a function producing sorted JSON already exists. It worths to reduce code duplication.

* @returns {string} A unified diff string representing the changes.
*/
const convertChangesToUnifiedDiffString = (changes: Change[]) => {
const unifiedDiff: string = unidiff.formatLines(changes, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: TS can infer string type for unifiedDiff

Suggested change
const unifiedDiff: string = unidiff.formatLines(changes, {
const unifiedDiff = unidiff.formatLines(changes, {

diff_outcome: ThreeWayDiffOutcome.StockValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Target,
has_update: true,
conflict: ThreeWayDiffConflict.NONE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Input data should be accurate. You can implement some function to generate input data to mitigate such situations. The problem here is that some can use storybook's data as a reference and might be confused by an unexpected conflict value.

diff_outcome: ThreeWayDiffOutcome.CustomizedValueNoUpdate,
merge_outcome: ThreeWayMergeOutcome.Current,
has_update: false,
conflict: ThreeWayDiffConflict.NONE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct. I double checked it with the RFC. A ThreeWayDiffConflict's comment is a bit misleading though.

diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
has_update: true,
conflict: ThreeWayDiffConflict.SOLVABLE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right. I doubled checked the RFC and multi-line field has a solvable conflict in this case.

Btw the file you linked here doesn't exist anymore.

diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate,
merge_outcome: ThreeWayMergeOutcome.Merged,
has_update: true,
conflict: ThreeWayDiffConflict.SOLVABLE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it's beneficial during development phase in storybook to verify the component works with real multiline data. Though preparing and maintaining such data is tricky since spotting differences manually takes time. Let's use simplified abstract text later for testing and leave current text here unchanged.

newFieldValue?: DiffableAllFields[FieldName]
): SubfieldChange[] => {
switch (fieldName) {
/* Typecasting here because narrowing a union type (`DiffableAllFields[FieldName]`) is not supported by TS */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/* Typecasting here because narrowing a union type (`DiffableAllFields[FieldName]`) is not supported by TS */
/* Typecasting `oldFieldValue` and `newFieldValue` to corresponding field type `DiffableAllFields[*]` is required here since `oldFieldValue` and `newFieldValue` concrete types depend on `fieldName` but TS isn't track that. */

* 2.0.
*/

export interface SubfieldChange {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed that it's good to define SubfieldChanges type as well looking like type SubfieldChanges = Readonly< SubfieldChange[]>;. Are there issues with that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. The issue is that if you make the array readonly, you won't be able to push into it when you construct it. But I have just figured that you can assign only the return type and it works then. Updated the code.

@nikitaindik nikitaindik marked this pull request as draft August 8, 2024 12:06
@nikitaindik nikitaindik marked this pull request as ready for review August 8, 2024 12:09
@nikitaindik nikitaindik enabled auto-merge (squash) August 8, 2024 12:11
@nikitaindik nikitaindik merged commit 3de37ce into elastic:main Aug 8, 2024
46 checks passed
@kibanamachine kibanamachine added the backport:skip This commit does not require backporting label Aug 8, 2024
@kibana-ci
Copy link
Collaborator

💛 Build succeeded, but was flaky

Failed CI Steps

Test Failures

  • [job] [logs] FTR Configs #35 / security app - login selector Basic functionality can login anonymously preserving original URL

Metrics [docs]

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
securitySolution 20.7MB 20.7MB +571.0B

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

cc @nikitaindik

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting Feature:Prebuilt Detection Rules Security Solution Prebuilt Detection Rules release_note:skip Skip the PR/issue when compiling release notes Team:Detection Rule Management Security Detection Rule Management Team Team:Detections and Resp Security Detection Response Team Team: SecuritySolution Security Solutions Team working on SIEM, Endpoint, Timeline, Resolver, etc. v8.16.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants