diff --git a/package.json b/package.json index bb666f88f738d..677575fed98ef 100644 --- a/package.json +++ b/package.json @@ -1017,6 +1017,7 @@ "react": "^17.0.2", "react-ace": "^7.0.5", "react-color": "^2.13.8", + "react-diff-viewer-continued": "^3.3.1", "react-dom": "^17.0.2", "react-dropzone": "^4.2.9", "react-fast-compare": "^2.0.4", diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/rule_diff_tab_react_diff_viewer_continued.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/rule_diff_tab_react_diff_viewer_continued.tsx new file mode 100644 index 0000000000000..37d29dbc3f1d7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/rule_diff_tab_react_diff_viewer_continued.tsx @@ -0,0 +1,160 @@ +/* + * 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, { useState, useContext, useMemo } from 'react'; +import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'; +import { EuiSpacer, EuiSwitch, EuiRadioGroup, useEuiTheme } from '@elastic/eui'; +import type { RuleResponse } from '../../../../../../common/api/detection_engine/model/rule_schema/rule_schemas.gen'; +import { sortAndStringifyJson } from './sort_stringify_json'; + +const CustomStylesContext = React.createContext({}); +const DiffMethodContext = React.createContext(DiffMethod.CHARS); + +interface CustomStylesProps { + children: React.ReactNode; +} + +const CustomStyles = ({ children }: CustomStylesProps) => { + const { euiTheme } = useEuiTheme(); + const [enabled, setEnabled] = useState(false); + + const customStyles = { + variables: { + light: { + addedBackground: 'transparent', + removedBackground: 'transparent', + }, + }, + wordAdded: { + background: 'transparent', + color: euiTheme.colors.successText, + }, + wordRemoved: { + background: 'transparent', + color: euiTheme.colors.dangerText, + textDecoration: 'line-through', + }, + }; + + return ( + + { + setEnabled(!enabled); + }} + /> + + {children} + + ); +}; + +interface WholeObjectDiffProps { + oldRule: RuleResponse; + newRule: RuleResponse; +} + +const WholeObjectDiff = ({ oldRule, newRule }: WholeObjectDiffProps) => { + const diffMethod = useContext(DiffMethodContext); + const styles = useContext(CustomStylesContext); + + const [oldSource, newSource] = useMemo(() => { + const oldSrc = + diffMethod === DiffMethod.JSON && typeof oldRule === 'object' + ? oldRule + : sortAndStringifyJson(oldRule); + + const newSrc = + diffMethod === DiffMethod.JSON && typeof newRule === 'object' + ? newRule + : sortAndStringifyJson(newRule); + + return [oldSrc, newSrc]; + }, [oldRule, newRule, diffMethod]); + + return ( + + ); +}; + +interface RuleDiffTabProps { + oldRule: RuleResponse; + newRule: RuleResponse; +} + +export const RuleDiffTabReactDiffViewerContinued = ({ oldRule, newRule }: RuleDiffTabProps) => { + const options = [ + { + id: DiffMethod.CHARS, + label: 'Chars', + }, + { + id: DiffMethod.WORDS, + label: 'Words', + }, + { + id: DiffMethod.LINES, + label: 'Lines', + }, + { + id: DiffMethod.SENTENCES, + label: 'Sentences', + }, + { + id: DiffMethod.JSON, + label: 'JSON', + }, + ]; + + const [compareMethod, setCompareMethod] = useState(DiffMethod.JSON); + + return ( + <> + + { + setCompareMethod(optionId as DiffMethod); + }} + legend={{ + children: {'Diffing algorthm'}, + }} + /> + + + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/sort_stringify_json.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/sort_stringify_json.ts new file mode 100644 index 0000000000000..a5b40bfea7695 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/json_diff/sort_stringify_json.ts @@ -0,0 +1,11 @@ +/* + * 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 stringify from 'json-stable-stringify'; + +export const sortAndStringifyJson = (jsObject: Record): string => + stringify(jsObject, { space: 2 }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_details_flyout.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_details_flyout.tsx index aa221b6cdb147..8d6ef88d92c74 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_details_flyout.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/rule_details_flyout.tsx @@ -95,7 +95,7 @@ const tabPaddingClassName = css` padding: 0 ${euiThemeVars.euiSizeM} ${euiThemeVars.euiSizeXL} ${euiThemeVars.euiSizeM}; `; -const TabContentPadding: React.FC = ({ children }) => ( +export const TabContentPadding: React.FC = ({ children }) => (
{children}
); @@ -104,6 +104,7 @@ interface RuleDetailsFlyoutProps { ruleActions?: React.ReactNode; dataTestSubj?: string; closeFlyout: () => void; + getRuleTabs?: (rule: RuleResponse, defaultTabs: EuiTabbedContentTab[]) => EuiTabbedContentTab[]; } export const RuleDetailsFlyout = ({ @@ -111,6 +112,7 @@ export const RuleDetailsFlyout = ({ ruleActions, dataTestSubj, closeFlyout, + getRuleTabs, }: RuleDetailsFlyoutProps) => { const { expandedOverviewSections, toggleOverviewSection } = useOverviewTabSections(); @@ -145,12 +147,13 @@ export const RuleDetailsFlyout = ({ ); const tabs = useMemo(() => { + const defaultTabs = [overviewTab]; if (rule.note) { - return [overviewTab, investigationGuideTab]; - } else { - return [overviewTab]; + defaultTabs.push(investigationGuideTab); } - }, [overviewTab, investigationGuideTab, rule.note]); + + return getRuleTabs ? getRuleTabs(rule, defaultTabs) : defaultTabs; + }, [overviewTab, investigationGuideTab, rule, getRuleTabs]); const [selectedTabId, setSelectedTabId] = useState(tabs[0].id); const selectedTab = tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0]; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx index 290f85ade3a03..0b2092385e173 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/upgrade_prebuilt_rules_table/upgrade_prebuilt_rules_table_context.tsx @@ -24,11 +24,16 @@ import type { UpgradePrebuiltRulesTableFilterOptions } from './use_filter_prebui import { useFilterPrebuiltRulesToUpgrade } from './use_filter_prebuilt_rules_to_upgrade'; import { useAsyncConfirmation } from '../rules_table/use_async_confirmation'; import { useRuleDetailsFlyout } from '../../../../rule_management/components/rule_details/use_rule_details_flyout'; -import { RuleDetailsFlyout } from '../../../../rule_management/components/rule_details/rule_details_flyout'; +import { + RuleDetailsFlyout, + TabContentPadding, +} from '../../../../rule_management/components/rule_details/rule_details_flyout'; import * as i18n from './translations'; import { MlJobUpgradeModal } from '../../../../../detections/components/modals/ml_job_upgrade_modal'; +import { RuleDiffTabReactDiffViewerContinued } from '../../../../rule_management/components/rule_details/json_diff/rule_diff_tab_react_diff_viewer_continued'; + export interface UpgradePrebuiltRulesTableState { /** * Rules available to be updated @@ -286,6 +291,27 @@ export const UpgradePrebuiltRulesTableContextProvider = ({ {i18n.UPDATE_BUTTON_LABEL} } + getRuleTabs={(rule, defaultTabs) => { + const activeRule = filteredRules.find(({ id }) => rule.id); + if (!activeRule) { + return defaultTabs; + } + + const diffTabReactDiffViewerContinued = { + id: 'react-diff-viewer-continued', + name: 'react-diff-viewer-continued', + content: ( + + + + ), + }; + + return [diffTabReactDiffViewerContinued, ...defaultTabs]; + }} /> )} diff --git a/yarn.lock b/yarn.lock index e6b6c9eeef668..bc958e8e1c98f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1876,10 +1876,10 @@ "@emotion/utils" "0.11.3" babel-plugin-emotion "^10.0.27" -"@emotion/css@^11.11.0": - version "11.11.0" - resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.11.0.tgz#dad6a27a77d5e5cbb0287674c3ace76d762563ca" - integrity sha512-m4g6nKzZyiKyJ3WOfdwrBdcujVcpaScIWHAnyNKPm/A/xJKwfXPfQAbEVi1kgexWTDakmg+r2aDj0KvnMTo4oQ== +"@emotion/css@^11.11.0", "@emotion/css@^11.11.2": + version "11.11.2" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.11.2.tgz#e5fa081d0c6e335352e1bc2b05953b61832dca5a" + integrity sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew== dependencies: "@emotion/babel-plugin" "^11.11.0" "@emotion/cache" "^11.11.0" @@ -14888,7 +14888,7 @@ diff-sequences@^29.4.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== -diff@5.0.0, diff@^5.0.0: +diff@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== @@ -14908,6 +14908,11 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@^5.0.0, diff@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" + integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + diffie-hellman@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" @@ -25305,6 +25310,17 @@ react-colorful@^5.1.2: resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784" integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg== +react-diff-viewer-continued@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/react-diff-viewer-continued/-/react-diff-viewer-continued-3.3.1.tgz#1ef6af86fc92ad721a5461f8f3c44f74381ea81d" + integrity sha512-YhjWjCUq6cs8k9iErpWh/xB2jFCndigGAz2TKubdqrSTtDH5Ib+tdQgzBWVXMMqgtEwoPLi+WFmSsdSoYbDVpw== + dependencies: + "@emotion/css" "^11.11.2" + classnames "^2.3.2" + diff "^5.1.0" + memoize-one "^6.0.0" + prop-types "^15.8.1" + react-docgen-typescript@^2.0.0, react-docgen-typescript@^2.1.1: version "2.2.2" resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c"