mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.12`: - [[Security Solution] JSON diff view for prebuilt rule upgrade flow (#172535)](https://github.com/elastic/kibana/pull/172535) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Nikita Indik","email":"nikita.indik@elastic.co"},"sourceCommit":{"committedDate":"2023-12-08T15:16:42Z","message":"[Security Solution] JSON diff view for prebuilt rule upgrade flow (#172535)\n\n## Summary\r\n\r\n**Resolves: https://github.com/elastic/kibana/issues/169160**\r\n**Resolves: https://github.com/elastic/kibana/issues/166164**\r\n**Docs issue: https://github.com/elastic/security-docs/issues/4371**\r\n\r\nThis PR adds a new \"Updates\" tab to the prebuilt rules upgrade flyout.\r\nThis tab shows a diff between the installed and updated rule JSON\r\nrepresentations.\r\n\r\n<img width=\"1313\" alt=\"Schermafbeelding 2023-12-05 om 02 48 37\"\r\nsrc=\"ec0f95c6
-22c6-4ce6-a6cc-0ceee974c6f7\">\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] Functional changes are communicated to the Docs team. A ticket or\r\nPR is opened in https://github.com/elastic/security-docs. The following\r\ninformation is included: any feature flags used, affected environments\r\n(Serverless, ESS, or both). ([Docs\r\nissue](https://github.com/elastic/security-docs/issues/4371))\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials ([Docs\r\nissue](https://github.com/elastic/security-docs/issues/4371))\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios (will be added\r\nin a follow-up PR)\r\n- [ ] Functional changes are covered with a test plan and automated\r\ntests (will be added in a follow-up PR)\r\n- [x] Any UI touched in this PR is usable by keyboard only (learn more\r\nabout [keyboard accessibility](https://webaim.org/techniques/keyboard/))\r\n- [x] Any UI touched in this PR does not create any new axe failures\r\n(run axe in browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n- [x] This renders correctly on smaller devices using a responsive\r\nlayout. (Doesn't look great on phone screen, because viewing diff\r\nrequires a lot of horizontal space. Tablets are fine though.)\r\n- [x] This was checked for [cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n- [x] Functional changes are hidden behind a feature flag. If not\r\nhidden, the PR explains why these changes are being implemented in a\r\nlong-living feature branch.\r\n- [x] Comprehensive manual testing is done by two engineers: the PR\r\nauthor and one of the PR reviewers. Changes are tested in both ESS and\r\nServerless.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Georgii Gorbachev <georgii.gorbachev@elastic.co>","sha":"e5a6b978b8eca4ac275b72e88415e2238315a241","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Detections and Resp","Team: SecuritySolution","release_note:feature","Team:Detection Rule Management","Feature:Prebuilt Detection Rules","v8.12.0","v8.13.0"],"number":172535,"url":"https://github.com/elastic/kibana/pull/172535","mergeCommit":{"message":"[Security Solution] JSON diff view for prebuilt rule upgrade flow (#172535)\n\n## Summary\r\n\r\n**Resolves: https://github.com/elastic/kibana/issues/169160**\r\n**Resolves: https://github.com/elastic/kibana/issues/166164**\r\n**Docs issue: https://github.com/elastic/security-docs/issues/4371**\r\n\r\nThis PR adds a new \"Updates\" tab to the prebuilt rules upgrade flyout.\r\nThis tab shows a diff between the installed and updated rule JSON\r\nrepresentations.\r\n\r\n<img width=\"1313\" alt=\"Schermafbeelding 2023-12-05 om 02 48 37\"\r\nsrc=\"ec0f95c6
-22c6-4ce6-a6cc-0ceee974c6f7\">\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] Functional changes are communicated to the Docs team. A ticket or\r\nPR is opened in https://github.com/elastic/security-docs. The following\r\ninformation is included: any feature flags used, affected environments\r\n(Serverless, ESS, or both). ([Docs\r\nissue](https://github.com/elastic/security-docs/issues/4371))\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials ([Docs\r\nissue](https://github.com/elastic/security-docs/issues/4371))\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios (will be added\r\nin a follow-up PR)\r\n- [ ] Functional changes are covered with a test plan and automated\r\ntests (will be added in a follow-up PR)\r\n- [x] Any UI touched in this PR is usable by keyboard only (learn more\r\nabout [keyboard accessibility](https://webaim.org/techniques/keyboard/))\r\n- [x] Any UI touched in this PR does not create any new axe failures\r\n(run axe in browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n- [x] This renders correctly on smaller devices using a responsive\r\nlayout. (Doesn't look great on phone screen, because viewing diff\r\nrequires a lot of horizontal space. Tablets are fine though.)\r\n- [x] This was checked for [cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n- [x] Functional changes are hidden behind a feature flag. If not\r\nhidden, the PR explains why these changes are being implemented in a\r\nlong-living feature branch.\r\n- [x] Comprehensive manual testing is done by two engineers: the PR\r\nauthor and one of the PR reviewers. Changes are tested in both ESS and\r\nServerless.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Georgii Gorbachev <georgii.gorbachev@elastic.co>","sha":"e5a6b978b8eca4ac275b72e88415e2238315a241"}},"sourceBranch":"main","suggestedTargetBranches":["8.12"],"targetPullRequestStates":[{"branch":"8.12","label":"v8.12.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/172535","number":172535,"mergeCommit":{"message":"[Security Solution] JSON diff view for prebuilt rule upgrade flow (#172535)\n\n## Summary\r\n\r\n**Resolves: https://github.com/elastic/kibana/issues/169160**\r\n**Resolves: https://github.com/elastic/kibana/issues/166164**\r\n**Docs issue: https://github.com/elastic/security-docs/issues/4371**\r\n\r\nThis PR adds a new \"Updates\" tab to the prebuilt rules upgrade flyout.\r\nThis tab shows a diff between the installed and updated rule JSON\r\nrepresentations.\r\n\r\n<img width=\"1313\" alt=\"Schermafbeelding 2023-12-05 om 02 48 37\"\r\nsrc=\"ec0f95c6
-22c6-4ce6-a6cc-0ceee974c6f7\">\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] Functional changes are communicated to the Docs team. A ticket or\r\nPR is opened in https://github.com/elastic/security-docs. The following\r\ninformation is included: any feature flags used, affected environments\r\n(Serverless, ESS, or both). ([Docs\r\nissue](https://github.com/elastic/security-docs/issues/4371))\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials ([Docs\r\nissue](https://github.com/elastic/security-docs/issues/4371))\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios (will be added\r\nin a follow-up PR)\r\n- [ ] Functional changes are covered with a test plan and automated\r\ntests (will be added in a follow-up PR)\r\n- [x] Any UI touched in this PR is usable by keyboard only (learn more\r\nabout [keyboard accessibility](https://webaim.org/techniques/keyboard/))\r\n- [x] Any UI touched in this PR does not create any new axe failures\r\n(run axe in browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n- [x] This renders correctly on smaller devices using a responsive\r\nlayout. (Doesn't look great on phone screen, because viewing diff\r\nrequires a lot of horizontal space. Tablets are fine though.)\r\n- [x] This was checked for [cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n- [x] Functional changes are hidden behind a feature flag. If not\r\nhidden, the PR explains why these changes are being implemented in a\r\nlong-living feature branch.\r\n- [x] Comprehensive manual testing is done by two engineers: the PR\r\nauthor and one of the PR reviewers. Changes are tested in both ESS and\r\nServerless.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Georgii Gorbachev <georgii.gorbachev@elastic.co>","sha":"e5a6b978b8eca4ac275b72e88415e2238315a241"}}]}] BACKPORT--> Co-authored-by: Nikita Indik <nikita.indik@elastic.co>
This commit is contained in:
parent
230722552d
commit
692bde9cf3
19 changed files with 1004 additions and 70 deletions
|
@ -922,6 +922,7 @@
|
|||
"deep-freeze-strict": "^1.1.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"del": "^6.1.0",
|
||||
"diff": "^5.1.0",
|
||||
"elastic-apm-node": "^4.2.0",
|
||||
"email-addresses": "^5.0.0",
|
||||
"execa": "^5.1.1",
|
||||
|
@ -1029,6 +1030,7 @@
|
|||
"react": "^17.0.2",
|
||||
"react-ace": "^7.0.5",
|
||||
"react-color": "^2.13.8",
|
||||
"react-diff-view": "^3.2.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^4.2.9",
|
||||
"react-fast-compare": "^2.0.4",
|
||||
|
@ -1089,6 +1091,7 @@
|
|||
"type-detect": "^4.0.8",
|
||||
"typescript-fsa": "^3.0.0",
|
||||
"typescript-fsa-reducers": "^1.2.2",
|
||||
"unidiff": "^1.0.4",
|
||||
"unified": "9.2.2",
|
||||
"use-resize-observer": "^9.1.0",
|
||||
"usng.js": "^0.4.5",
|
||||
|
@ -1345,6 +1348,7 @@
|
|||
"@types/dedent": "^0.7.0",
|
||||
"@types/deep-freeze-strict": "^1.1.0",
|
||||
"@types/delete-empty": "^2.0.0",
|
||||
"@types/diff": "^5.0.8",
|
||||
"@types/ejs": "^3.0.6",
|
||||
"@types/enzyme": "^3.10.12",
|
||||
"@types/eslint": "^8.44.2",
|
||||
|
@ -1508,7 +1512,6 @@
|
|||
"debug": "^2.6.9",
|
||||
"delete-empty": "^2.0.0",
|
||||
"dependency-check": "^4.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"dpdm": "3.9.0",
|
||||
"ejs": "^3.1.8",
|
||||
"enzyme": "^3.11.0",
|
||||
|
|
|
@ -53,10 +53,10 @@ it('rewrites ftr reports with minimal changes', async () => {
|
|||
reportPath: Path.resolve(__dirname, './__fixtures__/ftr_report.xml'),
|
||||
});
|
||||
|
||||
expect(createPatch('ftr.xml', FTR_REPORT, xml, { context: 0 })).toMatchInlineSnapshot(`
|
||||
expect(createPatch('ftr.xml', FTR_REPORT, xml)).toMatchInlineSnapshot(`
|
||||
Index: ftr.xml
|
||||
===================================================================
|
||||
--- ftr.xml [object Object]
|
||||
--- ftr.xml
|
||||
+++ ftr.xml
|
||||
@@ -1,53 +1,56 @@
|
||||
‹?xml version="1.0" encoding="utf-8"?›
|
||||
|
@ -149,10 +149,10 @@ it('rewrites jest reports with minimal changes', async () => {
|
|||
reportPath: Path.resolve(__dirname, './__fixtures__/jest_report.xml'),
|
||||
});
|
||||
|
||||
expect(createPatch('jest.xml', JEST_REPORT, xml, { context: 0 })).toMatchInlineSnapshot(`
|
||||
expect(createPatch('jest.xml', JEST_REPORT, xml)).toMatchInlineSnapshot(`
|
||||
Index: jest.xml
|
||||
===================================================================
|
||||
--- jest.xml [object Object]
|
||||
--- jest.xml
|
||||
+++ jest.xml
|
||||
@@ -3,13 +3,17 @@
|
||||
‹testsuite name="x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts" timestamp="2019-06-07T03:42:21" time="14.504" tests="5" failures="1" skipped="0" file="/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts"›
|
||||
|
@ -196,10 +196,10 @@ it('rewrites mocha reports with minimal changes', async () => {
|
|||
reportPath: Path.resolve(__dirname, './__fixtures__/mocha_report.xml'),
|
||||
});
|
||||
|
||||
expect(createPatch('mocha.xml', MOCHA_REPORT, xml, { context: 0 })).toMatchInlineSnapshot(`
|
||||
expect(createPatch('mocha.xml', MOCHA_REPORT, xml)).toMatchInlineSnapshot(`
|
||||
Index: mocha.xml
|
||||
===================================================================
|
||||
--- mocha.xml [object Object]
|
||||
--- mocha.xml
|
||||
+++ mocha.xml
|
||||
@@ -1,13 +1,16 @@
|
||||
‹?xml version="1.0" encoding="utf-8"?›
|
||||
|
@ -273,10 +273,10 @@ it('rewrites cypress reports with minimal changes', async () => {
|
|||
reportPath: Path.resolve(__dirname, './__fixtures__/cypress_report.xml'),
|
||||
});
|
||||
|
||||
expect(createPatch('cypress.xml', CYPRESS_REPORT, xml, { context: 0 })).toMatchInlineSnapshot(`
|
||||
expect(createPatch('cypress.xml', CYPRESS_REPORT, xml)).toMatchInlineSnapshot(`
|
||||
Index: cypress.xml
|
||||
===================================================================
|
||||
--- cypress.xml [object Object]
|
||||
--- cypress.xml
|
||||
+++ cypress.xml
|
||||
@@ -1,25 +1,16 @@
|
||||
-‹?xml version="1.0" encoding="UTF-8"?›
|
||||
|
|
|
@ -156,6 +156,13 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
* Enables SentinelOne manual host manipulation actions
|
||||
*/
|
||||
sentinelOneManualHostActionsEnabled: false,
|
||||
|
||||
/*
|
||||
* Enables experimental "Updates" tab in the prebuilt rule upgrade flyout.
|
||||
* This tab shows the JSON diff between the installed prebuilt rule
|
||||
* version and the latest available version.
|
||||
*/
|
||||
jsonPrebuiltRulesDiffingEnabled: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -5,4 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['50%', '50%'];
|
||||
export const DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['50%', '50%'];
|
||||
export const LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS: [string, string] = ['30%', '70%'];
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { css, Global } from '@emotion/react';
|
||||
import {
|
||||
Diff,
|
||||
useSourceExpansion,
|
||||
useMinCollapsedLines,
|
||||
parseDiff,
|
||||
tokenize,
|
||||
} from 'react-diff-view';
|
||||
import 'react-diff-view/style/index.css';
|
||||
import type {
|
||||
RenderGutter,
|
||||
HunkData,
|
||||
TokenizeOptions,
|
||||
DiffProps,
|
||||
HunkTokens,
|
||||
} from 'react-diff-view';
|
||||
import unidiff from 'unidiff';
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { Hunks } from './hunks';
|
||||
import { markEdits, DiffMethod } from './mark_edits';
|
||||
|
||||
interface UseExpandReturn {
|
||||
expandRange: (start: number, end: number) => void;
|
||||
hunks: HunkData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HunkData[]} hunks - An array of hunk objects representing changes in a section of a string. Sections normally span multiple lines.
|
||||
* @param {string} oldSource - Original string, before changes
|
||||
* @returns {UseExpandReturn} - "expandRange" is function that triggers expansion, "hunks" is an array of hunks with hidden section expanded.
|
||||
*
|
||||
* @description
|
||||
* Sections of diff without changes are hidden by default, because they are not present in the "hunks" array.
|
||||
* "useExpand" allows to show these hidden sections when user clicks on "Expand hidden <number> lines" button.
|
||||
* Calling "expandRange" basically merges two adjacent hunks into one:
|
||||
* - takes first hunk
|
||||
* - appends all the lines between the first hunk and the second hunk
|
||||
* - finally appends the second hunk
|
||||
* returned "hunks" is the resulting array of hunks with hidden section expanded.
|
||||
*/
|
||||
const useExpand = (hunks: HunkData[], oldSource: string): UseExpandReturn => {
|
||||
const [hunksWithSourceExpanded, expandRange] = useSourceExpansion(hunks, oldSource);
|
||||
const hunksWithMinLinesCollapsed = useMinCollapsedLines(0, hunksWithSourceExpanded, oldSource);
|
||||
|
||||
return {
|
||||
expandRange,
|
||||
hunks: hunksWithMinLinesCollapsed,
|
||||
};
|
||||
};
|
||||
|
||||
const useTokens = (
|
||||
hunks: HunkData[],
|
||||
diffMethod: DiffMethod,
|
||||
oldSource: string
|
||||
): HunkTokens | undefined => {
|
||||
if (!hunks) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const options: TokenizeOptions = {
|
||||
oldSource,
|
||||
highlight: false,
|
||||
enhancers: [
|
||||
/*
|
||||
This custom "markEdits" function is a slightly modified version of "markEdits"
|
||||
enhancer from react-diff-view with added support for word-level highlighting.
|
||||
*/
|
||||
markEdits(hunks, diffMethod),
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
/*
|
||||
Synchroniously apply all the enhancers to the hunks and return an array of tokens.
|
||||
*/
|
||||
return tokenize(hunks, options);
|
||||
} catch (ex) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const renderGutter: RenderGutter = ({ change }) => {
|
||||
/*
|
||||
Custom gutter: rendering "+" or "-" so the diff is readable by colorblind people.
|
||||
*/
|
||||
if (change.type === 'insert') {
|
||||
return <span>{'+'}</span>;
|
||||
}
|
||||
|
||||
if (change.type === 'delete') {
|
||||
return <span>{'-'}</span>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const convertToDiffFile = (oldSource: string, newSource: string) => {
|
||||
/*
|
||||
"diffLines" call converts two strings of text into an array of Change objects.
|
||||
*/
|
||||
const changes = unidiff.diffLines(oldSource, newSource);
|
||||
|
||||
/*
|
||||
Then "formatLines" takes an array of Change objects and turns it into a single "unified diff" string.
|
||||
More info about the "unified diff" format: https://en.wikipedia.org/wiki/Diff_utility#Unified_format
|
||||
Unified diff is a string with change markers added. Looks something like:
|
||||
`
|
||||
@@ -3,16 +3,15 @@
|
||||
"author": ["Elastic"],
|
||||
- "from": "now-540s",
|
||||
+ "from": "now-9m",
|
||||
"history_window_start": "now-14d",
|
||||
`
|
||||
*/
|
||||
const unifiedDiff: string = unidiff.formatLines(changes, {
|
||||
context: 3,
|
||||
});
|
||||
|
||||
/*
|
||||
"parseDiff" converts a unified diff string into a gitdiff-parser File object.
|
||||
|
||||
File object contains some metadata and the "hunks" property - an array of Hunk objects.
|
||||
Hunks represent changed lines of code plus a few unchanged lines above and below for context.
|
||||
*/
|
||||
const [diffFile] = parseDiff(unifiedDiff, {
|
||||
nearbySequences: 'zip',
|
||||
});
|
||||
|
||||
return diffFile;
|
||||
};
|
||||
|
||||
const TABLE_CLASS_NAME = 'rule-update-diff-table';
|
||||
const CODE_CLASS_NAME = 'rule-update-diff-code';
|
||||
const GUTTER_CLASS_NAME = 'rule-update-diff-gutter';
|
||||
|
||||
const CustomStyles: React.FC = ({ children }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const customCss = css`
|
||||
.${TABLE_CLASS_NAME} .diff-gutter-col {
|
||||
width: ${euiTheme.size.xl};
|
||||
}
|
||||
|
||||
.${CODE_CLASS_NAME}.diff-code, .${GUTTER_CLASS_NAME}.diff-gutter {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.${GUTTER_CLASS_NAME}:nth-child(3) {
|
||||
border-left: 1px solid ${euiTheme.colors.mediumShade};
|
||||
}
|
||||
|
||||
.${GUTTER_CLASS_NAME}.diff-gutter-delete {
|
||||
color: ${euiTheme.colors.dangerText};
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.${GUTTER_CLASS_NAME}.diff-gutter-insert {
|
||||
color: ${euiTheme.colors.successText};
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.${CODE_CLASS_NAME}.diff-code {
|
||||
padding: 0 ${euiTheme.size.l} 0 ${euiTheme.size.m};
|
||||
}
|
||||
|
||||
.${CODE_CLASS_NAME}.diff-code-delete .diff-code-edit,
|
||||
.${CODE_CLASS_NAME}.diff-code-insert .diff-code-edit {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.${CODE_CLASS_NAME}.diff-code-delete .diff-code-edit {
|
||||
color: ${euiTheme.colors.dangerText};
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.${CODE_CLASS_NAME}.diff-code-insert .diff-code-edit {
|
||||
color: ${euiTheme.colors.successText};
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Global styles={customCss} />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DiffViewProps extends Partial<DiffProps> {
|
||||
oldSource: string;
|
||||
newSource: string;
|
||||
diffMethod?: DiffMethod;
|
||||
}
|
||||
|
||||
export const DiffView = ({
|
||||
oldSource,
|
||||
newSource,
|
||||
diffMethod = DiffMethod.WORDS,
|
||||
}: DiffViewProps) => {
|
||||
/*
|
||||
"react-diff-view" components consume diffs not as a strings, but as something they call "hunks".
|
||||
So we first need to convert our "before" and "after" strings into these "hunk" objects.
|
||||
"hunks" describe changed sections of code plus a few unchanged lines above and below for context.
|
||||
*/
|
||||
|
||||
/*
|
||||
"diffFile" is essentially an object containing an array of hunks plus some metadata.
|
||||
*/
|
||||
const diffFile = useMemo(() => convertToDiffFile(oldSource, newSource), [oldSource, newSource]);
|
||||
|
||||
/*
|
||||
Sections of diff without changes are hidden by default, because they are not present in the "hunks" array.
|
||||
"useExpand" allows to show these hidden sections when a user clicks on "Expand hidden <number> lines" button.
|
||||
*/
|
||||
const { expandRange, hunks } = useExpand(diffFile.hunks, oldSource);
|
||||
|
||||
/*
|
||||
Go over each hunk and extract tokens from it. For example, split strings into words or characters,
|
||||
so we can highlight them later.
|
||||
*/
|
||||
const tokens = useTokens(hunks, diffMethod, oldSource);
|
||||
|
||||
return (
|
||||
<CustomStyles>
|
||||
<Diff
|
||||
/*
|
||||
"diffType": can be either 'add', 'delete', 'modify', 'rename' or 'copy'.
|
||||
Passing 'add' or 'delete' would skip rendering one of the sides in split view.
|
||||
*/
|
||||
diffType={diffFile.type}
|
||||
hunks={hunks}
|
||||
renderGutter={renderGutter}
|
||||
tokens={tokens}
|
||||
className={TABLE_CLASS_NAME}
|
||||
gutterClassName={GUTTER_CLASS_NAME}
|
||||
codeClassName={CODE_CLASS_NAME}
|
||||
>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-shadow */}
|
||||
{(hunks) => <Hunks hunks={hunks} oldSource={oldSource} expandRange={expandRange} />}
|
||||
</Diff>
|
||||
</CustomStyles>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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, { useCallback } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Hunk, Decoration, getCollapsedLinesCountBetween } from 'react-diff-view';
|
||||
import type { HunkData, DecorationProps } from 'react-diff-view';
|
||||
import { EuiSpacer, EuiIcon, EuiLink, EuiFlexGroup, EuiText } from '@elastic/eui';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface UnfoldButtonProps extends Omit<DecorationProps, 'children'> {
|
||||
start: number;
|
||||
end: number;
|
||||
onExpand: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
const UnfoldButton = ({ start, end, onExpand, ...props }: UnfoldButtonProps) => {
|
||||
const expand = useCallback(() => onExpand(start, end), [onExpand, start, end]);
|
||||
|
||||
const linesCount = end - start;
|
||||
|
||||
return (
|
||||
<Decoration {...props}>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
{start > 1 && <EuiSpacer size="m" />}
|
||||
<EuiFlexGroup justifyContent="center">
|
||||
<EuiLink onClick={expand}>
|
||||
<EuiFlexGroup justifyContent="center" alignItems="center" gutterSize="s">
|
||||
<EuiIcon type="sortable" />
|
||||
<EuiText size="s">{i18n.EXPAND_UNCHANGED_LINES(linesCount)}</EuiText>
|
||||
</EuiFlexGroup>
|
||||
</EuiLink>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiFlexGroup>
|
||||
</Decoration>
|
||||
);
|
||||
};
|
||||
|
||||
interface UnfoldCollapsedProps {
|
||||
previousHunk: HunkData;
|
||||
currentHunk?: HunkData;
|
||||
linesCount: number;
|
||||
onExpand: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
const UnfoldCollapsed = ({
|
||||
previousHunk,
|
||||
currentHunk,
|
||||
linesCount,
|
||||
onExpand,
|
||||
}: UnfoldCollapsedProps) => {
|
||||
if (!currentHunk) {
|
||||
const nextStart = previousHunk.oldStart + previousHunk.oldLines;
|
||||
const collapsedLines = linesCount - nextStart + 1;
|
||||
|
||||
if (collapsedLines <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <UnfoldButton start={nextStart} end={linesCount + 1} onExpand={onExpand} />;
|
||||
}
|
||||
|
||||
const collapsedLines = getCollapsedLinesCountBetween(previousHunk, currentHunk);
|
||||
|
||||
if (!previousHunk) {
|
||||
if (!collapsedLines) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <UnfoldButton start={1} end={currentHunk.oldStart} onExpand={onExpand} />;
|
||||
}
|
||||
|
||||
const collapsedStart = previousHunk.oldStart + previousHunk.oldLines;
|
||||
const collapsedEnd = currentHunk.oldStart;
|
||||
|
||||
return <UnfoldButton start={collapsedStart} end={collapsedEnd} onExpand={onExpand} />;
|
||||
};
|
||||
|
||||
interface HunksProps {
|
||||
hunks: HunkData[];
|
||||
oldSource: string;
|
||||
expandRange: (start: number, end: number) => void;
|
||||
}
|
||||
|
||||
export const Hunks = ({ hunks, oldSource, expandRange }: HunksProps) => {
|
||||
const linesCount = oldSource.split('\n').length;
|
||||
|
||||
const hunkElements = hunks.reduce((children: ReactElement[], hunk: HunkData, index: number) => {
|
||||
const previousElement = children[children.length - 1];
|
||||
|
||||
children.push(
|
||||
<UnfoldCollapsed
|
||||
key={`decoration-${hunk.content}`}
|
||||
previousHunk={previousElement && previousElement.props.hunk}
|
||||
currentHunk={hunk}
|
||||
linesCount={linesCount}
|
||||
onExpand={expandRange}
|
||||
/>
|
||||
);
|
||||
|
||||
children.push(<Hunk key={`hunk-${hunk.content}`} hunk={hunk} />);
|
||||
|
||||
const isLastHunk = index === hunks.length - 1;
|
||||
if (isLastHunk && oldSource) {
|
||||
children.push(
|
||||
<UnfoldCollapsed
|
||||
key="decoration-tail"
|
||||
previousHunk={hunk}
|
||||
linesCount={linesCount}
|
||||
onExpand={expandRange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}, []);
|
||||
|
||||
return <>{hunkElements}</>;
|
||||
};
|
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
* 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 { findIndex, flatMap, flatten } from 'lodash';
|
||||
import * as diff from 'diff';
|
||||
import type { Change as DiffJsChange } from 'diff';
|
||||
import { isDelete, isInsert, isNormal, pickRanges } from 'react-diff-view';
|
||||
import type { ChangeData, HunkData, RangeTokenNode, TokenizeEnhancer } from 'react-diff-view';
|
||||
|
||||
enum DmpChangeType {
|
||||
DELETE = -1,
|
||||
EQUAL = 0,
|
||||
INSERT = 1,
|
||||
}
|
||||
|
||||
type Diff = [DmpChangeType, string];
|
||||
|
||||
type StringDiffFn = (oldString: string, newString: string) => DiffJsChange[];
|
||||
|
||||
interface JsDiff {
|
||||
diffChars: StringDiffFn;
|
||||
diffWords: StringDiffFn;
|
||||
diffWordsWithSpace: StringDiffFn;
|
||||
diffLines: StringDiffFn;
|
||||
diffTrimmedLines: StringDiffFn;
|
||||
diffSentences: StringDiffFn;
|
||||
diffCss: StringDiffFn;
|
||||
}
|
||||
|
||||
const jsDiff: JsDiff = diff;
|
||||
|
||||
export enum DiffMethod {
|
||||
CHARS = 'diffChars',
|
||||
WORDS = 'diffWords',
|
||||
WORDS_WITH_SPACE = 'diffWordsWithSpace',
|
||||
LINES = 'diffLines',
|
||||
TRIMMED_LINES = 'diffTrimmedLines',
|
||||
SENTENCES = 'diffSentences',
|
||||
CSS = 'diffCss',
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ChangeData[]} changes - An array representing the changes in the block.
|
||||
* Each hunk represents a section of a string and includes information about the changes in that section.
|
||||
* Sections normally span multiple lines.
|
||||
* @param {DiffMethod} diffMethod - Diffing algorithm to use for token extraction. For example, "diffWords" will tokenize the string into words.
|
||||
*
|
||||
* @returns {TokenizeEnhancer} A react-diff-view plugin that processes diff hunks and returns an array of tokens.
|
||||
* Tokens are then used to render "added" / "removed" diff highlighting.
|
||||
*
|
||||
* @description
|
||||
* Converts the given ChangeData array to two strings representing the old source and new source of a change block.
|
||||
* The format of the strings is as follows:
|
||||
*/
|
||||
function findChangeBlocks(changes: ChangeData[]): ChangeData[][] {
|
||||
const start = findIndex(changes, (change) => !isNormal(change));
|
||||
|
||||
if (start === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const end = findIndex(changes, (change) => !!isNormal(change), start);
|
||||
|
||||
if (end === -1) {
|
||||
return [changes.slice(start)];
|
||||
}
|
||||
|
||||
return [changes.slice(start, end), ...findChangeBlocks(changes.slice(end))];
|
||||
}
|
||||
|
||||
function groupDiffs(diffs: Diff[]): [Diff[], Diff[]] {
|
||||
return diffs.reduce<[Diff[], Diff[]]>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
([oldDiffs, newDiffs], diff) => {
|
||||
const [type] = diff;
|
||||
|
||||
switch (type) {
|
||||
case DmpChangeType.INSERT:
|
||||
newDiffs.push(diff);
|
||||
break;
|
||||
case DmpChangeType.DELETE:
|
||||
oldDiffs.push(diff);
|
||||
break;
|
||||
default:
|
||||
oldDiffs.push(diff);
|
||||
newDiffs.push(diff);
|
||||
break;
|
||||
}
|
||||
|
||||
return [oldDiffs, newDiffs];
|
||||
},
|
||||
[[], []]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Diff[]} diffs An array of changes in the diff-match-patch format
|
||||
* @returns {Diff[][]} An array of arrays, where changes are grouped by a line number.
|
||||
*/
|
||||
function splitDiffToLines(diffs: Diff[]): Diff[][] {
|
||||
return diffs.reduce<Diff[][]>(
|
||||
(lines, [type, value]) => {
|
||||
const currentLines = value.split('\n');
|
||||
|
||||
const [currentLineRemaining, ...nextLines] = currentLines.map(
|
||||
(line: string): Diff => [type, line]
|
||||
);
|
||||
const next: Diff[][] = [
|
||||
...lines.slice(0, -1),
|
||||
[...lines[lines.length - 1], currentLineRemaining],
|
||||
...nextLines.map((line) => [line]),
|
||||
];
|
||||
return next;
|
||||
},
|
||||
[[]]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Diff[]} diffs An array of changes within a single line in the diff-match-patch format
|
||||
* @param {number} lineNumber Line number where the changes are found
|
||||
* @returns {RangeTokenNode[]} Array of "edit" objects where each item contains
|
||||
* info about line number and start / end character positions.
|
||||
*/
|
||||
function diffsToEdits(diffs: Diff[], lineNumber: number): RangeTokenNode[] {
|
||||
const output = diffs.reduce<[RangeTokenNode[], number]>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
(output, diff) => {
|
||||
const [edits, start] = output;
|
||||
const [type, value] = diff;
|
||||
if (type !== DmpChangeType.EQUAL) {
|
||||
const edit: RangeTokenNode = {
|
||||
type: 'edit',
|
||||
lineNumber,
|
||||
start,
|
||||
length: value.length,
|
||||
};
|
||||
edits.push(edit);
|
||||
}
|
||||
|
||||
return [edits, start + value.length];
|
||||
},
|
||||
[[], 0]
|
||||
);
|
||||
|
||||
return output[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Diff[][]} linesOfDiffs - Changes in a diff-match-patch format, grouped by a line number.
|
||||
* @param {number} startLineNumber - Line number of the first line.
|
||||
* @returns {RangeTokenNode[]} Flattened array of "edit" objects where each item contains
|
||||
* info about line number and start / end character positions.
|
||||
*/
|
||||
function convertToLinesOfEdits(linesOfDiffs: Diff[][], startLineNumber: number): RangeTokenNode[] {
|
||||
return flatMap(linesOfDiffs, (diffs, i) => diffsToEdits(diffs, startLineNumber + i));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiffMethod} diffMethod - Diffing algorithm to use for token extraction.
|
||||
* @param {string} oldSource - A substring of the original source string.
|
||||
* @param {string} newSource - A corresponding substring of the new source string.
|
||||
* @returns {[Diff[], Diff[]]} Two arrays of changes in the diff-match-patch format.
|
||||
* Every item is a tuple of two values: [<change type: addition, deletion or unchanged>, <substring>].
|
||||
*
|
||||
* @description Runs two strings through the chosen diffing algorithm using the "diff" library to determine
|
||||
* which parts of the original string were added / removed / unchanged. Then returns an array of changes in
|
||||
* the diff-match-patch diff format.
|
||||
*/
|
||||
function diffBy(diffMethod: DiffMethod, oldSource: string, newSource: string): [Diff[], Diff[]] {
|
||||
/* Diff two substrings using the "diff" library */
|
||||
const jsDiffChanges: DiffJsChange[] = jsDiff[diffMethod](oldSource, newSource);
|
||||
/* Convert the result to the diff-match-patch format, because that's the format react-diff-view methods expect */
|
||||
const diffs: Diff[] = diff.convertChangesToDMP(jsDiffChanges);
|
||||
|
||||
if (diffs.length <= 1) {
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
/* Split diff-match-patch formatted diffs into two arrays: one for the old source and one for the new source */
|
||||
return groupDiffs(diffs);
|
||||
}
|
||||
|
||||
const getLineNumber = (change: ChangeData | undefined) => {
|
||||
if (!change || isNormal(change)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return change.lineNumber;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ChangeData[]} changes - An array of сhange objects. Each change object represents changes in a single line.
|
||||
* @param {DiffMethod} diffMethod - Diffing algorithm to use for token extraction.
|
||||
* @returns {[RangeTokenNode[], RangeTokenNode[]]} A tuple containing two arrays of RangeTokenNodes - one for
|
||||
* the old source and another one for the new source. Each RangeTokenNode contains information about line numbers
|
||||
* and character positions of changes.
|
||||
*
|
||||
* @description This function processes change objects and determines exactly which segments of the orginal string changed.
|
||||
* It diffs old and new substrings and computes at which character position each change starts and ends,
|
||||
* taking the diffing algorithm into account (by char, by word, by sentence, etc.)
|
||||
*/
|
||||
function diffChangeBlock(
|
||||
changes: ChangeData[],
|
||||
diffMethod: DiffMethod
|
||||
): [RangeTokenNode[], RangeTokenNode[]] {
|
||||
/*
|
||||
Convert an array of change objects into two strings representing the old source and the new source of a change block.
|
||||
Basically, recreate parts of the original strings from change objects so we can pass these strings to the text diffing library.
|
||||
*/
|
||||
const [oldSourceSnippet, newSourceSnippet] = changes.reduce(
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
([oldSourceSnippet, newSourceSnippet], change) =>
|
||||
isDelete(change)
|
||||
? [oldSourceSnippet + (oldSourceSnippet ? '\n' : '') + change.content, newSourceSnippet]
|
||||
: [oldSourceSnippet, newSourceSnippet + (newSourceSnippet ? '\n' : '') + change.content],
|
||||
['', '']
|
||||
);
|
||||
|
||||
/*
|
||||
* Run the chosen diffing algorithm with an "old" and a "new" substrings as input.
|
||||
* The result is an array of changes in the diff-match-patch format.
|
||||
*/
|
||||
const [oldDiffs, newDiffs] = diffBy(diffMethod, oldSourceSnippet, newSourceSnippet);
|
||||
|
||||
if (oldDiffs.length === 0 && newDiffs.length === 0) {
|
||||
return [[], []];
|
||||
}
|
||||
|
||||
const oldStartLineNumber = getLineNumber(changes.find(isDelete));
|
||||
const newStartLineNumber = getLineNumber(changes.find(isInsert));
|
||||
|
||||
if (oldStartLineNumber === undefined || newStartLineNumber === undefined) {
|
||||
throw new Error('Could not find start line number for edit');
|
||||
}
|
||||
|
||||
/*
|
||||
* Group changes by a line number they are found in, then determine start / end character
|
||||
* positions of each change.
|
||||
*/
|
||||
const oldEdits = convertToLinesOfEdits(splitDiffToLines(oldDiffs), oldStartLineNumber);
|
||||
const newEdits = convertToLinesOfEdits(splitDiffToLines(newDiffs), newStartLineNumber);
|
||||
|
||||
return [oldEdits, newEdits];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HunkData[]} hunks - An array of hunk objects.
|
||||
* Each hunk represents a section of a string and includes information about the changes in that section.
|
||||
* Sections normally span multiple lines.
|
||||
* @param {DiffMethod} diffMethod - Diffing algorithm to use for token extraction. For example, "diffWords" will tokenize the string into words.
|
||||
*
|
||||
* @returns {TokenizeEnhancer} A react-diff-view plugin that processes diff hunks and returns an array of tokens.
|
||||
* Tokens are then used to render "added" / "removed" diff highlighting.
|
||||
*
|
||||
* @description
|
||||
* Converts the given ChangeData array to two strings representing the old source and new source of a change block.
|
||||
* The format of the strings is as follows:
|
||||
*/
|
||||
export function markEdits(hunks: HunkData[], diffMethod: DiffMethod): TokenizeEnhancer {
|
||||
/*
|
||||
changeBlocks is an array that contains information about the lines that have changes (additions or deletions).
|
||||
Unchanged lines are not included.
|
||||
*/
|
||||
const changeBlocks = flatMap(
|
||||
hunks.map((hunk) => hunk.changes),
|
||||
findChangeBlocks
|
||||
);
|
||||
|
||||
const [oldEdits, newEdits] = changeBlocks
|
||||
/*
|
||||
diffChangeBlock diffs two substrings and determines character positions of changes,
|
||||
taking the diffing algorithm into account (by char, by word, by sentence, etc.)
|
||||
*/
|
||||
.map((changes) => diffChangeBlock(changes, diffMethod))
|
||||
.reduce(
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
([oldEdits, newEdits], [currentOld, currentNew]) => [
|
||||
oldEdits.concat(currentOld),
|
||||
newEdits.concat(currentNew),
|
||||
],
|
||||
[[], []]
|
||||
);
|
||||
|
||||
return pickRanges(flatten(oldEdits), flatten(newEdits));
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const EXPAND_UNCHANGED_LINES = (linesCount: number) =>
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.upgradeRules.expandHiddenDiffLinesLabel',
|
||||
{
|
||||
values: { linesCount },
|
||||
defaultMessage:
|
||||
'Expand {linesCount} unchanged {linesCount, plural, one {line} other {lines}}',
|
||||
}
|
||||
);
|
||||
|
||||
export const BASE_VERSION = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.upgradeRules.baseVersionLabel',
|
||||
{
|
||||
defaultMessage: 'Base version',
|
||||
}
|
||||
);
|
||||
|
||||
export const BASE_VERSION_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.upgradeRules.baseVersionDescriptionLabel',
|
||||
{
|
||||
defaultMessage: 'Shows currently installed rule',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATED_VERSION = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.upgradeRules.updatedVersionLabel',
|
||||
{
|
||||
defaultMessage: 'Update',
|
||||
}
|
||||
);
|
||||
|
||||
export const UPDATED_VERSION_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.rules.upgradeRules.updatedVersionDescriptionLabel',
|
||||
{
|
||||
defaultMessage: 'Shows rule that will be installed',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare module 'unidiff' {
|
||||
interface Change {
|
||||
count?: number | undefined;
|
||||
value: string;
|
||||
added?: boolean | undefined;
|
||||
removed?: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface FormatOptions {
|
||||
context?: number;
|
||||
}
|
||||
|
||||
export function diffLines(x: string, y: string): Change[];
|
||||
|
||||
export function formatLines(line: Change[], options?: FormatOptions): string;
|
||||
}
|
|
@ -33,7 +33,7 @@ import { filterEmptyThreats } from '../../../rule_creation_ui/pages/rule_creatio
|
|||
import { ThreatEuiFlexGroup } from '../../../../detections/components/rules/description_step/threat_description';
|
||||
|
||||
import { BadgeList } from './badge_list';
|
||||
import { DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants';
|
||||
import { DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const OverrideColumn = styled(EuiFlexItem)`
|
||||
|
@ -426,12 +426,14 @@ const prepareAboutSectionListItems = (
|
|||
|
||||
export interface RuleAboutSectionProps extends React.ComponentProps<typeof EuiDescriptionList> {
|
||||
rule: Partial<RuleResponse>;
|
||||
columnWidths?: EuiDescriptionListProps['columnWidths'];
|
||||
hideName?: boolean;
|
||||
hideDescription?: boolean;
|
||||
}
|
||||
|
||||
export const RuleAboutSection = ({
|
||||
rule,
|
||||
columnWidths = DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS,
|
||||
hideName,
|
||||
hideDescription,
|
||||
...descriptionListProps
|
||||
|
@ -445,7 +447,7 @@ export const RuleAboutSection = ({
|
|||
type={descriptionListProps.type ?? 'column'}
|
||||
rowGutterSize={descriptionListProps.rowGutterSize ?? 'm'}
|
||||
listItems={aboutSectionListItems}
|
||||
columnWidths={DESCRIPTION_LIST_COLUMN_WIDTHS}
|
||||
columnWidths={columnWidths}
|
||||
data-test-subj="listItemColumnStepRuleDescription"
|
||||
{...descriptionListProps}
|
||||
/>
|
||||
|
|
|
@ -52,7 +52,7 @@ import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/
|
|||
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
|
||||
import { TechnicalPreviewBadge } from '../../../../detections/components/rules/technical_preview_badge';
|
||||
import { BadgeList } from './badge_list';
|
||||
import { DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants';
|
||||
import { DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants';
|
||||
import * as i18n from './translations';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import type { ExperimentalFeatures } from '../../../../../common/experimental_features';
|
||||
|
@ -724,6 +724,7 @@ const prepareDefinitionSectionListItems = (
|
|||
export interface RuleDefinitionSectionProps
|
||||
extends React.ComponentProps<typeof EuiDescriptionList> {
|
||||
rule: Partial<RuleResponse>;
|
||||
columnWidths?: EuiDescriptionListProps['columnWidths'];
|
||||
isInteractive?: boolean;
|
||||
dataTestSubj?: string;
|
||||
}
|
||||
|
@ -731,6 +732,7 @@ export interface RuleDefinitionSectionProps
|
|||
export const RuleDefinitionSection = ({
|
||||
rule,
|
||||
isInteractive = false,
|
||||
columnWidths = DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS,
|
||||
dataTestSubj,
|
||||
...descriptionListProps
|
||||
}: RuleDefinitionSectionProps) => {
|
||||
|
@ -756,7 +758,7 @@ export const RuleDefinitionSection = ({
|
|||
type={descriptionListProps.type ?? 'column'}
|
||||
rowGutterSize={descriptionListProps.rowGutterSize ?? 'm'}
|
||||
listItems={definitionSectionListItems}
|
||||
columnWidths={DESCRIPTION_LIST_COLUMN_WIDTHS}
|
||||
columnWidths={columnWidths}
|
||||
data-test-subj="listItemColumnStepRuleDescription"
|
||||
{...descriptionListProps}
|
||||
/>
|
||||
|
|
|
@ -21,11 +21,15 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import type { EuiTabbedContentTab, EuiTabbedContentProps } from '@elastic/eui';
|
||||
import type { EuiTabbedContentTab, EuiTabbedContentProps, EuiFlyoutProps } from '@elastic/eui';
|
||||
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { RuleOverviewTab, useOverviewTabSections } from './rule_overview_tab';
|
||||
import { RuleInvestigationGuideTab } from './rule_investigation_guide_tab';
|
||||
import {
|
||||
DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS,
|
||||
LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS,
|
||||
} from './constants';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -95,13 +99,15 @@ const tabPaddingClassName = css`
|
|||
padding: 0 ${euiThemeVars.euiSizeM} ${euiThemeVars.euiSizeXL} ${euiThemeVars.euiSizeM};
|
||||
`;
|
||||
|
||||
const TabContentPadding: React.FC = ({ children }) => (
|
||||
export const TabContentPadding: React.FC = ({ children }) => (
|
||||
<div className={tabPaddingClassName}>{children}</div>
|
||||
);
|
||||
|
||||
interface RuleDetailsFlyoutProps {
|
||||
rule: RuleResponse;
|
||||
ruleActions?: React.ReactNode;
|
||||
size?: EuiFlyoutProps['size'];
|
||||
extraTabs?: EuiTabbedContentTab[];
|
||||
dataTestSubj?: string;
|
||||
closeFlyout: () => void;
|
||||
}
|
||||
|
@ -109,6 +115,8 @@ interface RuleDetailsFlyoutProps {
|
|||
export const RuleDetailsFlyout = ({
|
||||
rule,
|
||||
ruleActions,
|
||||
size = 'm',
|
||||
extraTabs = [],
|
||||
dataTestSubj,
|
||||
closeFlyout,
|
||||
}: RuleDetailsFlyoutProps) => {
|
||||
|
@ -122,13 +130,18 @@ export const RuleDetailsFlyout = ({
|
|||
<TabContentPadding>
|
||||
<RuleOverviewTab
|
||||
rule={rule}
|
||||
columnWidths={
|
||||
size === 'l'
|
||||
? LARGE_DESCRIPTION_LIST_COLUMN_WIDTHS
|
||||
: DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS
|
||||
}
|
||||
expandedOverviewSections={expandedOverviewSections}
|
||||
toggleOverviewSection={toggleOverviewSection}
|
||||
/>
|
||||
</TabContentPadding>
|
||||
),
|
||||
}),
|
||||
[rule, expandedOverviewSections, toggleOverviewSection]
|
||||
[rule, size, expandedOverviewSections, toggleOverviewSection]
|
||||
);
|
||||
|
||||
const investigationGuideTab: EuiTabbedContentTab = useMemo(
|
||||
|
@ -146,11 +159,11 @@ export const RuleDetailsFlyout = ({
|
|||
|
||||
const tabs = useMemo(() => {
|
||||
if (rule.note) {
|
||||
return [overviewTab, investigationGuideTab];
|
||||
return [...extraTabs, overviewTab, investigationGuideTab];
|
||||
} else {
|
||||
return [overviewTab];
|
||||
return [...extraTabs, overviewTab];
|
||||
}
|
||||
}, [overviewTab, investigationGuideTab, rule.note]);
|
||||
}, [overviewTab, investigationGuideTab, rule.note, extraTabs]);
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState<string>(tabs[0].id);
|
||||
const selectedTab = tabs.find((tab) => tab.id === selectedTabId) ?? tabs[0];
|
||||
|
@ -168,7 +181,7 @@ export const RuleDetailsFlyout = ({
|
|||
|
||||
return (
|
||||
<EuiFlyout
|
||||
size="m"
|
||||
size={size}
|
||||
onClose={closeFlyout}
|
||||
ownFocus={false}
|
||||
key="prebuilt-rules-flyout"
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiPanel,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGroup,
|
||||
EuiTitle,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema/rule_schemas.gen';
|
||||
import { DiffView } from './json_diff/diff_view';
|
||||
import * as i18n from './json_diff/translations';
|
||||
|
||||
const sortAndStringifyJson = (jsObject: Record<string, unknown>): string =>
|
||||
stringify(jsObject, { space: 2 });
|
||||
|
||||
interface RuleDiffTabProps {
|
||||
oldRule: RuleResponse;
|
||||
newRule: RuleResponse;
|
||||
}
|
||||
|
||||
export const RuleDiffTab = ({ oldRule, newRule }: RuleDiffTabProps) => {
|
||||
const [oldSource, newSource] = useMemo(() => {
|
||||
const visibleOldRuleProperties = omit(oldRule, 'revision');
|
||||
const visibleNewRuleProperties = omit(newRule, 'revision');
|
||||
|
||||
return [
|
||||
sortAndStringifyJson(visibleOldRuleProperties),
|
||||
sortAndStringifyJson(visibleNewRuleProperties),
|
||||
];
|
||||
}, [oldRule, newRule]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel color="transparent" hasBorder>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexGroup alignItems="baseline" gutterSize="xs">
|
||||
<EuiIconTip
|
||||
color="subdued"
|
||||
content={i18n.BASE_VERSION_DESCRIPTION}
|
||||
type="iInCircle"
|
||||
size="m"
|
||||
display="block"
|
||||
/>
|
||||
<EuiTitle size="xxxs">
|
||||
<h6>{i18n.BASE_VERSION}</h6>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup alignItems="baseline" gutterSize="xs">
|
||||
<EuiIconTip
|
||||
color="subdued"
|
||||
content={i18n.UPDATED_VERSION_DESCRIPTION}
|
||||
type="iInCircle"
|
||||
size="m"
|
||||
/>
|
||||
<EuiTitle size="xxxs">
|
||||
<h6>{i18n.UPDATED_VERSION}</h6>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule margin="s" size="full" />
|
||||
<DiffView oldSource={oldSource} newSource={newSource} />
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -14,11 +14,13 @@ import {
|
|||
EuiHorizontalRule,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import type { EuiDescriptionListProps } from '@elastic/eui';
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { RuleAboutSection, Description } from './rule_about_section';
|
||||
import { RuleDefinitionSection } from './rule_definition_section';
|
||||
import { RuleScheduleSection } from './rule_schedule_section';
|
||||
import { RuleSetupGuideSection } from './rule_setup_guide_section';
|
||||
import { DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -87,52 +89,56 @@ const ExpandableSection = ({ title, isOpen, toggle, children }: ExpandableSectio
|
|||
|
||||
interface RuleOverviewTabProps {
|
||||
rule: RuleResponse;
|
||||
columnWidths?: EuiDescriptionListProps['columnWidths'];
|
||||
expandedOverviewSections: Record<keyof typeof defaultOverviewOpenSections, boolean>;
|
||||
toggleOverviewSection: Record<keyof typeof defaultOverviewOpenSections, () => void>;
|
||||
}
|
||||
|
||||
export const RuleOverviewTab = ({
|
||||
rule,
|
||||
columnWidths = DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS,
|
||||
expandedOverviewSections,
|
||||
toggleOverviewSection,
|
||||
}: RuleOverviewTabProps) => (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<ExpandableSection
|
||||
title={i18n.ABOUT_SECTION_LABEL}
|
||||
isOpen={expandedOverviewSections.about}
|
||||
toggle={toggleOverviewSection.about}
|
||||
>
|
||||
{rule.description && <Description description={rule.description} />}
|
||||
<RuleAboutSection rule={rule} hideDescription hideName />
|
||||
</ExpandableSection>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<ExpandableSection
|
||||
title={i18n.DEFINITION_SECTION_LABEL}
|
||||
isOpen={expandedOverviewSections.definition}
|
||||
toggle={toggleOverviewSection.definition}
|
||||
>
|
||||
<RuleDefinitionSection rule={rule} />
|
||||
</ExpandableSection>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<ExpandableSection
|
||||
title={i18n.SCHEDULE_SECTION_LABEL}
|
||||
isOpen={expandedOverviewSections.schedule}
|
||||
toggle={toggleOverviewSection.schedule}
|
||||
>
|
||||
<RuleScheduleSection rule={rule} />
|
||||
</ExpandableSection>
|
||||
{rule.setup && (
|
||||
<>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<ExpandableSection
|
||||
title={i18n.SETUP_GUIDE_SECTION_LABEL}
|
||||
isOpen={expandedOverviewSections.setup}
|
||||
toggle={toggleOverviewSection.setup}
|
||||
>
|
||||
<RuleSetupGuideSection setup={rule.setup} />
|
||||
</ExpandableSection>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}: RuleOverviewTabProps) => {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<ExpandableSection
|
||||
title={i18n.ABOUT_SECTION_LABEL}
|
||||
isOpen={expandedOverviewSections.about}
|
||||
toggle={toggleOverviewSection.about}
|
||||
>
|
||||
{rule.description && <Description description={rule.description} />}
|
||||
<RuleAboutSection rule={rule} columnWidths={columnWidths} hideDescription hideName />
|
||||
</ExpandableSection>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<ExpandableSection
|
||||
title={i18n.DEFINITION_SECTION_LABEL}
|
||||
isOpen={expandedOverviewSections.definition}
|
||||
toggle={toggleOverviewSection.definition}
|
||||
>
|
||||
<RuleDefinitionSection rule={rule} columnWidths={columnWidths} />
|
||||
</ExpandableSection>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<ExpandableSection
|
||||
title={i18n.SCHEDULE_SECTION_LABEL}
|
||||
isOpen={expandedOverviewSections.schedule}
|
||||
toggle={toggleOverviewSection.schedule}
|
||||
>
|
||||
<RuleScheduleSection rule={rule} columnWidths={columnWidths} />
|
||||
</ExpandableSection>
|
||||
{rule.setup && (
|
||||
<>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<ExpandableSection
|
||||
title={i18n.SETUP_GUIDE_SECTION_LABEL}
|
||||
isOpen={expandedOverviewSections.setup}
|
||||
toggle={toggleOverviewSection.setup}
|
||||
>
|
||||
<RuleSetupGuideSection setup={rule.setup} />
|
||||
</ExpandableSection>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiDescriptionList, EuiText } from '@elastic/eui';
|
||||
import type { EuiDescriptionListProps } from '@elastic/eui';
|
||||
import type { RuleResponse } from '../../../../../common/api/detection_engine/model/rule_schema';
|
||||
import { getHumanizedDuration } from '../../../../detections/pages/detection_engine/rules/helpers';
|
||||
import { DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants';
|
||||
import { DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS } from './constants';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface IntervalProps {
|
||||
|
@ -35,10 +36,12 @@ const From = ({ from, interval }: FromProps) => (
|
|||
|
||||
export interface RuleScheduleSectionProps extends React.ComponentProps<typeof EuiDescriptionList> {
|
||||
rule: Partial<RuleResponse>;
|
||||
columnWidths?: EuiDescriptionListProps['columnWidths'];
|
||||
}
|
||||
|
||||
export const RuleScheduleSection = ({
|
||||
rule,
|
||||
columnWidths = DEFAULT_DESCRIPTION_LIST_COLUMN_WIDTHS,
|
||||
...descriptionListProps
|
||||
}: RuleScheduleSectionProps) => {
|
||||
if (!rule.interval || !rule.from) {
|
||||
|
@ -64,7 +67,7 @@ export const RuleScheduleSection = ({
|
|||
type={descriptionListProps.type ?? 'column'}
|
||||
rowGutterSize={descriptionListProps.rowGutterSize ?? 'm'}
|
||||
listItems={ruleSectionListItems}
|
||||
columnWidths={DESCRIPTION_LIST_COLUMN_WIDTHS}
|
||||
columnWidths={columnWidths}
|
||||
{...descriptionListProps}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -21,6 +21,13 @@ export const INVESTIGATION_GUIDE_TAB_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const UPDATES_TAB_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDetails.updatesTabLabel',
|
||||
{
|
||||
defaultMessage: 'Updates',
|
||||
}
|
||||
);
|
||||
|
||||
export const DISMISS_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.detectionEngine.ruleDetails.dismissButtonLabel',
|
||||
{
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import type { EuiTabbedContentTab } from '@elastic/eui';
|
||||
import { useIsUpgradingSecurityPackages } from '../../../../rule_management/logic/use_upgrade_security_packages';
|
||||
import { useInstalledSecurityJobs } from '../../../../../common/components/ml/hooks/use_installed_security_jobs';
|
||||
import { useBoolState } from '../../../../../common/hooks/use_bool_state';
|
||||
|
@ -24,10 +25,15 @@ 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 * as i18n from './translations';
|
||||
|
||||
import {
|
||||
RuleDetailsFlyout,
|
||||
TabContentPadding,
|
||||
} from '../../../../rule_management/components/rule_details/rule_details_flyout';
|
||||
import { RuleDiffTab } from '../../../../rule_management/components/rule_details/rule_diff_tab';
|
||||
import { MlJobUpgradeModal } from '../../../../../detections/components/modals/ml_job_upgrade_modal';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import * as ruleDetailsI18n from '../../../../rule_management/components/rule_details/translations';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface UpgradePrebuiltRulesTableState {
|
||||
/**
|
||||
|
@ -111,6 +117,10 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
tags: [],
|
||||
});
|
||||
|
||||
const isJsonPrebuiltRulesDiffingEnabled = useIsExperimentalFeatureEnabled(
|
||||
'jsonPrebuiltRulesDiffingEnabled'
|
||||
);
|
||||
|
||||
const isUpgradingSecurityPackages = useIsUpgradingSecurityPackages();
|
||||
|
||||
const {
|
||||
|
@ -257,6 +267,29 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
actions,
|
||||
]);
|
||||
|
||||
const extraTabs = useMemo<EuiTabbedContentTab[]>(() => {
|
||||
const activeRule =
|
||||
isJsonPrebuiltRulesDiffingEnabled &&
|
||||
previewedRule &&
|
||||
filteredRules.find(({ id }) => id === previewedRule.id);
|
||||
|
||||
if (!activeRule) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'updates',
|
||||
name: ruleDetailsI18n.UPDATES_TAB_LABEL,
|
||||
content: (
|
||||
<TabContentPadding>
|
||||
<RuleDiffTab oldRule={activeRule.current_rule} newRule={activeRule.target_rule} />
|
||||
</TabContentPadding>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [previewedRule, filteredRules, isJsonPrebuiltRulesDiffingEnabled]);
|
||||
|
||||
return (
|
||||
<UpgradePrebuiltRulesTableContext.Provider value={providerValue}>
|
||||
<>
|
||||
|
@ -271,6 +304,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
{previewedRule && (
|
||||
<RuleDetailsFlyout
|
||||
rule={previewedRule}
|
||||
size={isJsonPrebuiltRulesDiffingEnabled ? 'l' : 'm'}
|
||||
dataTestSubj="updatePrebuiltRulePreview"
|
||||
closeFlyout={closeRulePreview}
|
||||
ruleActions={
|
||||
|
@ -286,6 +320,7 @@ export const UpgradePrebuiltRulesTableContextProvider = ({
|
|||
{i18n.UPDATE_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
}
|
||||
extraTabs={extraTabs}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -100,6 +100,11 @@ const calculateRuleInfos = (results: CalculateRuleDiffResult[]): RuleUpgradeInfo
|
|||
const targetRule: RuleResponse = {
|
||||
...convertPrebuiltRuleAssetToRuleResponse(targetVersion),
|
||||
id: installedCurrentVersion.id,
|
||||
revision: installedCurrentVersion.revision + 1,
|
||||
created_at: installedCurrentVersion.created_at,
|
||||
created_by: installedCurrentVersion.created_by,
|
||||
updated_at: new Date().toISOString(),
|
||||
updated_by: installedCurrentVersion.updated_by,
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
45
yarn.lock
45
yarn.lock
|
@ -9021,6 +9021,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/delete-empty/-/delete-empty-2.0.0.tgz#1647ae9e68f708a6ba778531af667ec55bc61964"
|
||||
integrity sha512-sq+kwx8zA9BSugT9N+Jr8/uWjbHMZ+N/meJEzRyT3gmLq/WMtx/iSIpvdpmBUi/cvXl6Kzpvve8G2ESkabFwmg==
|
||||
|
||||
"@types/diff@^5.0.8":
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.8.tgz#28dc501cc3e7c62d4c5d096afe20755170acf276"
|
||||
integrity sha512-kR0gRf0wMwpxQq6ME5s+tWk9zVCfJUl98eRkD05HWWRbhPB/eu4V1IbyZAsvzC1Gn4znBJ0HN01M4DGXdBEV8Q==
|
||||
|
||||
"@types/ejs@^3.0.6":
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.0.6.tgz#aca442289df623bfa8e47c23961f0357847b83fe"
|
||||
|
@ -14949,7 +14954,7 @@ diacritics@^1.3.0:
|
|||
resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1"
|
||||
integrity sha1-PvqHMj67hj5mls67AILUj/PW96E=
|
||||
|
||||
diff-match-patch@^1.0.0, diff-match-patch@^1.0.4:
|
||||
diff-match-patch@^1.0.0, diff-match-patch@^1.0.4, diff-match-patch@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
|
||||
integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
|
||||
|
@ -14969,7 +14974,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==
|
||||
|
@ -14989,6 +14994,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"
|
||||
|
@ -17546,6 +17556,11 @@ git-hooks-list@1.0.3:
|
|||
resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-1.0.3.tgz#be5baaf78203ce342f2f844a9d2b03dba1b45156"
|
||||
integrity sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ==
|
||||
|
||||
gitdiff-parser@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/gitdiff-parser/-/gitdiff-parser-0.3.1.tgz#5eb3e66eb7862810ba962fab762134071601baa5"
|
||||
integrity sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ==
|
||||
|
||||
github-from-package@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
|
||||
|
@ -25360,6 +25375,18 @@ 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-view@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-diff-view/-/react-diff-view-3.2.0.tgz#8fbf04782d78423903a59202ce7533f6312c1cc3"
|
||||
integrity sha512-p58XoqMxgt71ujpiDQTs9Za3nqTawt1E4bTzKsYSqr8I8br6cjQj1b66HxGnV8Yrc6MD6iQPqS1aZiFoGqEw+g==
|
||||
dependencies:
|
||||
classnames "^2.3.2"
|
||||
diff-match-patch "^1.0.5"
|
||||
gitdiff-parser "^0.3.1"
|
||||
lodash "^4.17.21"
|
||||
shallow-equal "^3.1.0"
|
||||
warning "^4.0.3"
|
||||
|
||||
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"
|
||||
|
@ -27226,6 +27253,11 @@ shallow-copy@~0.0.1:
|
|||
resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170"
|
||||
integrity sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=
|
||||
|
||||
shallow-equal@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-3.1.0.tgz#e7a54bac629c7f248eff6c2f5b63122ba4320bec"
|
||||
integrity sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==
|
||||
|
||||
shallowequal@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
|
||||
|
@ -29525,6 +29557,13 @@ unicode-trie@^2.0.0:
|
|||
pako "^0.2.5"
|
||||
tiny-inflate "^1.0.0"
|
||||
|
||||
unidiff@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/unidiff/-/unidiff-1.0.4.tgz#45096a898285821c51e22e84be4215c05d6511cd"
|
||||
integrity sha512-ynU0vsAXw0ir8roa+xPCUHmnJ5goc5BTM2Kuc3IJd8UwgaeRs7VSD5+eeaQL+xp1JtB92hu/Zy/Lgy7RZcr1pQ==
|
||||
dependencies:
|
||||
diff "^5.1.0"
|
||||
|
||||
unified@9.2.0:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8"
|
||||
|
@ -30534,7 +30573,7 @@ walker@^1.0.7, walker@^1.0.8, walker@~1.0.5:
|
|||
dependencies:
|
||||
makeerror "1.0.12"
|
||||
|
||||
warning@^4.0.2:
|
||||
warning@^4.0.2, warning@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue