-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
[Security Solution] Add ComparisonSide
component
#189384
Conversation
/ci |
1 similar comment
/ci |
Pinging @elastic/security-detections-response (Team:Detections and Resp) |
Pinging @elastic/security-solution (Team: SecuritySolution) |
Pinging @elastic/security-detection-rule-management (Team:Detection Rule Management) |
05ffa28
to
d574848
Compare
/ci |
There was a problem hiding this 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", |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
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 => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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[] => { |
There was a problem hiding this comment.
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[]
?
There was a problem hiding this comment.
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 SubfieldChange
s readonly because we push into an array as we build it.
return stringify(fieldValue, { space: 2 }); | ||
}; | ||
|
||
export const getSubfieldChangesForDataSource = ( |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
oldFieldValue as RuleDataSource | undefined, | ||
newFieldValue as RuleDataSource | undefined |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Updated.
/ci |
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. |
There was a problem hiding this 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!
export const stringifyToSortedJson = (fieldValue: unknown): string => { | ||
if (fieldValue === undefined) { | ||
return ''; | ||
} | ||
|
||
if (typeof fieldValue === 'string') { | ||
return fieldValue; | ||
} | ||
|
||
return stringify(fieldValue, { space: 2 }); | ||
}; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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', | ||
]; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
There was a problem hiding this 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, { |
There was a problem hiding this comment.
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
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, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, | ||
merge_outcome: ThreeWayMergeOutcome.Merged, | ||
has_update: true, | ||
conflict: ThreeWayDiffConflict.SOLVABLE, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
diff_outcome: ThreeWayDiffOutcome.CustomizedValueCanUpdate, | ||
merge_outcome: ThreeWayMergeOutcome.Merged, | ||
has_update: true, | ||
conflict: ThreeWayDiffConflict.SOLVABLE, |
There was a problem hiding this comment.
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 */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/* 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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
…parison-side-update
💛 Build succeeded, but was flaky
Failed CI StepsTest Failures
Metrics [docs]Async chunks
History
To update your PR or re-run it, just comment with: cc @nikitaindik |
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 tohttp://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
)