diff --git a/packages/patternfly-3/patternfly-react-extensions/less/diffview.less b/packages/patternfly-3/patternfly-react-extensions/less/diffview.less new file mode 100644 index 00000000000..dd9eda8b9bb --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/less/diffview.less @@ -0,0 +1,47 @@ +@import '~react-diff-view/index.css'; + +@diff-light-green-200: #cdffd8; +@diff-light-green-100: #e6ffed; +@diff-green: #acf2bd; +@diff-light-red: #ffeef0; +@diff-red: #fdb8c0; + +.diff-pf.diff { + border: 1px solid @color-pf-black-300; + max-height: 400px; + overflow: auto; + clear: both; + + .diff-line { + line-height: 1.7; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; + } + + .diff-gutter { + border-left: 1px solid @color-pf-black-300; + } + + .diff-gutter.diff-gutter-insert { + background-color: @diff-light-green-200; + } + + .diff-hunk-header { + border: 1px solid @color-pf-black-300; + } + + .diff-code-insert { + background-color: @diff-light-green-100; + + mark.diff-code-edit { + background-color: @diff-green; + } + } + + .diff-code-delete { + background-color: @diff-light-red; + + mark.diff-code-edit { + background-color: @diff-red; + } + } +} diff --git a/packages/patternfly-3/patternfly-react-extensions/less/patternfly-react-extensions.less b/packages/patternfly-3/patternfly-react-extensions/less/patternfly-react-extensions.less index 4d84fa8caed..58e1206e41e 100644 --- a/packages/patternfly-3/patternfly-react-extensions/less/patternfly-react-extensions.less +++ b/packages/patternfly-3/patternfly-react-extensions/less/patternfly-react-extensions.less @@ -6,3 +6,4 @@ @import 'properties-side-panel'; @import 'table-grid'; @import 'vertical-tabs'; +@import 'diff-view'; diff --git a/packages/patternfly-3/patternfly-react-extensions/package.json b/packages/patternfly-3/patternfly-react-extensions/package.json index aedc81ca5e0..18cacf0631f 100644 --- a/packages/patternfly-3/patternfly-react-extensions/package.json +++ b/packages/patternfly-3/patternfly-react-extensions/package.json @@ -44,7 +44,9 @@ "patternfly": "^3.58.0", "patternfly-react": "^2.28.2", "react-bootstrap": "^0.32.1", - "react-virtualized": "9.x" + "react-diff-view": "^1.8.1", + "react-virtualized": "9.x", + "unidiff": "^1.0.1" }, "peerDependencies": { "prop-types": "^15.6.1", diff --git a/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_diffview.scss b/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_diffview.scss new file mode 100644 index 00000000000..03aaf002442 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_diffview.scss @@ -0,0 +1,47 @@ +@import '~react-diff-view/index.css'; + +$diff-light-green-200: #cdffd8; +$diff-light-green-100: #e6ffed; +$diff-green: #acf2bd; +$diff-light-red: #ffeef0; +$diff-red: #fdb8c0; + +.diff-pf.diff { + border: 1px solid $color-pf-black-300; + max-height: 400px; + overflow: auto; + clear: both; + + .diff-line { + line-height: 1.7; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; + } + + .diff-gutter { + border-left: 1px solid $color-pf-black-300; + } + + .diff-gutter.diff-gutter-insert { + background-color: $diff-light-green-200; + } + + .diff-hunk-header { + border: 1px solid $color-pf-black-300; + } + + .diff-code-insert { + background-color: $diff-light-green-100; + + mark.diff-code-edit { + background-color: $diff-green; + } + } + + .diff-code-delete { + background-color: $diff-light-red; + + mark.diff-code-edit { + background-color: $diff-red; + } + } +} diff --git a/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_patternfly-react-extensions.scss b/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_patternfly-react-extensions.scss index 724313479d1..9cf211f0c53 100644 --- a/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_patternfly-react-extensions.scss +++ b/packages/patternfly-3/patternfly-react-extensions/sass/patternfly-react-extensions/_patternfly-react-extensions.scss @@ -9,3 +9,4 @@ @import 'properties-side-panel'; @import 'table-grid'; @import 'vertical-tabs'; +@import 'diffview'; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/DiffView.js b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/DiffView.js new file mode 100644 index 00000000000..469f0f6622f --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/DiffView.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { parseDiff, Diff, markCharacterEdits, markWordEdits } from 'react-diff-view'; +import { formatLines, diffLines } from 'unidiff'; + +const getDiff = (oldText, newText) => { + const diffText = formatLines(diffLines(oldText, newText)); + // these two lines are faked to mock git diff output + const header = ['diff --git a/a b/b', 'index 0000000..1111111 100644']; + return `${header.join('\n')}\n${diffText}`; +}; + +const DiffView = ({ + oldText, + newText, + className, + viewType, + patch, + markEditsByWord, + emptyState, + markThreshold, + markLongDistanceDiff, + ...props +}) => { + const markEditProps = { markThreshold, markLongDistanceDiff }; + const markEdits = markEditsByWord ? markWordEdits(markEditProps) : markCharacterEdits(markEditProps); + const classes = classNames('diff-pf', className); + + // Old, New Text + if (!patch) { + const gitDiff = getDiff(oldText, newText); + const files = parseDiff(gitDiff); + const hunk = files[0].hunks; + + if (hunk.length === 0) return emptyState; + return hunk && ; + } + + // Patch + const files = parseDiff(patch); + // eslint-disable-next-line react/prop-types + const renderFile = ({ oldRevision, newRevision, type, hunks }) => ( + + ); + + if (patch === '') return emptyState; + return
{files.map(renderFile)}
; +}; + +DiffView.propTypes = { + /** className */ + className: PropTypes.string, + /** oldText to compare */ + oldText: PropTypes.string, + /** newText to compare */ + newText: PropTypes.string, + /** viewType of the DiffView (Unified, Split) */ + viewType: PropTypes.string.isRequired, + /** git diff output */ + patch: PropTypes.string, + /** mark Diff by Words instead of Characters */ + markEditsByWord: PropTypes.bool, + /** emptyState node when there is no diff */ + emptyState: PropTypes.node.isRequired, + /** The maximum string distance when this function should try to mark edits */ + markThreshold: PropTypes.number, + /** If true, two strings with distance greater than threshold will create an edit containing the whole string */ + markLongDistanceDiff: PropTypes.bool +}; + +DiffView.defaultProps = { + oldText: null, + newText: null, + patch: null, + markEditsByWord: false, + markThreshold: 30, + markLongDistanceDiff: true, + className: '' +}; + +export default DiffView; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/DiffView.stories.js b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/DiffView.stories.js new file mode 100644 index 00000000000..cdc18077abd --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/DiffView.stories.js @@ -0,0 +1,49 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withInfo } from '@storybook/addon-info/dist/index'; +import { defaultTemplate } from 'storybook/decorators/storyTemplates'; +import { storybookPackageName, STORYBOOK_CATEGORY } from 'storybook/constants/siteConstants'; + +import { DiffView } from './index'; + +import { name } from '../../../package.json'; +import { boolean, text, select, withKnobs } from '@storybook/addon-knobs'; + +const stories = storiesOf(`${storybookPackageName(name)}/${STORYBOOK_CATEGORY.CONTENT_VIEWS}/DiffView`, module); +const emptyState =

No Diff

; + +stories.addDecorator( + defaultTemplate({ + title: 'Diff View', + description: ( +
+ The Diff View is based on the react-diff-view{' '} + package. This Component supports both git diff Patch and Old/New text inputs and displays the diff in Unified or + Split View. +
+ ) + }) +); + +stories.addDecorator(withKnobs); +stories.add( + 'DiffView', + withInfo({ + source: true, + propTables: [DiffView] + })(() => { + const viewType = select('View Type', { split: 'split', unified: 'unified' }, 'split'); + + return ( +
+ +
+ ); + }) +); diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/DiffView.test.js b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/DiffView.test.js new file mode 100644 index 00000000000..6e15f90cbc9 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/DiffView.test.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; + +import { DiffView } from './index'; +import { diffMock, patchMock, emptyState } from './_mocks_/DiffViewMocks'; + +test('Diff View renders w/Patch properly', () => { + const component = shallow(); + expect(toJson(component.render())).toMatchSnapshot(); +}); + +test('Diff View renders w/Text properly', () => { + const component = shallow(); + expect(toJson(component.render())).toMatchSnapshot(); +}); + +test('Diff View renders emptyState', () => { + const component = shallow(); + expect(toJson(component.render())).toMatchSnapshot(); +}); diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/__snapshots__/DiffView.test.js.snap b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/__snapshots__/DiffView.test.js.snap new file mode 100644 index 00000000000..d8dabdd28cb --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/__snapshots__/DiffView.test.js.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Diff View renders emptyState 1`] = ` +
+ empty +
+`; + +exports[`Diff View renders w/Patch properly 1`] = ` +
+ + + + + + +
+
+`; + +exports[`Diff View renders w/Text properly 1`] = ` + + + + + + + + + + + + + + + + +
+ + @@ -1 +1 @@ +
+ + + hello friend + + + + + hello + + there + + friend + +
+`; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/_mocks_/DiffViewMocks.js b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/_mocks_/DiffViewMocks.js new file mode 100644 index 00000000000..894cb5fa1d8 --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/_mocks_/DiffViewMocks.js @@ -0,0 +1,21 @@ +import React from 'react'; + +export const diffMock = { + oldText: 'hello friend', + newText: 'hello there friend', + viewType: 'split', + emptyState:
empty
+}; + +export const patchMock = { + viewType: 'unified', + patch: '---', + emptyState:
empty
+}; + +export const emptyState = { + oldText: 'hello friend', + newText: 'hello friend', + viewType: 'split', + emptyState:
empty
+}; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/index.js b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/index.js new file mode 100644 index 00000000000..08c77422f0a --- /dev/null +++ b/packages/patternfly-3/patternfly-react-extensions/src/components/DiffView/index.js @@ -0,0 +1,3 @@ +import DiffView from './DiffView'; + +export { DiffView }; diff --git a/packages/patternfly-3/patternfly-react-extensions/src/index.js b/packages/patternfly-3/patternfly-react-extensions/src/index.js index 34e0a66e9a8..659836a6908 100644 --- a/packages/patternfly-3/patternfly-react-extensions/src/index.js +++ b/packages/patternfly-3/patternfly-react-extensions/src/index.js @@ -6,3 +6,4 @@ export * from './components/FilterSidePanel'; export * from './components/PropertiesSidePanel'; export * from './components/TableGrid'; export * from './components/VerticalTabs'; +export * from './components/DiffView'; diff --git a/yarn.lock b/yarn.lock index dd6ce42a608..e0fc49c6871 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4299,7 +4299,7 @@ classnames@^2.2.0, classnames@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" -classnames@^2.2.3: +classnames@^2.2.3, classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" @@ -5825,6 +5825,11 @@ diff@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" +diff@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" + integrity sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k= + diff@^3.2.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -7755,6 +7760,11 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" +gitdiff-parser@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/gitdiff-parser/-/gitdiff-parser-0.1.2.tgz#26a256e05e9c2d5016b512a96c1dacb40862b92a" + integrity sha512-glDM6E1AwLYYTOPyI0CqamNEUSuwwAkmwULWpE2sHMpMZNzGJwErt7+eV+yIZcsbDza0pVSlwlBHFWbTf2Wu7A== + glamor@^2.20.40: version "2.20.40" resolved "https://registry.yarnpkg.com/glamor/-/glamor-2.20.40.tgz#f606660357b7cf18dface731ad1a2cfa93817f05" @@ -10163,6 +10173,11 @@ lodash.defaults@^4.0.1: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" +lodash.escape@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" + integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg= + lodash.filter@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" @@ -10171,6 +10186,11 @@ lodash.findkey@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.findkey/-/lodash.findkey-4.6.0.tgz#83058e903b51cbb759d09ccf546dea3ea39c4718" +lodash.findlastindex@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.findlastindex/-/lodash.findlastindex-4.6.0.tgz#b8375ac0f02e9b926375cdf8dc3ea814abf9c6ac" + integrity sha1-uDdawPAum5Jjdc343D6oFKv5xqw= + lodash.flatten@^4.2.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" @@ -10215,6 +10235,11 @@ lodash.map@^4.4.0, lodash.map@^4.5.1: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" +lodash.mapvalues@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c" + integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -13174,6 +13199,19 @@ react-dev-utils@^5.0.0: strip-ansi "3.0.1" text-table "0.2.0" +react-diff-view@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/react-diff-view/-/react-diff-view-1.8.1.tgz#0b9b4adcb92de6730d28177d68654dfcc2097f73" + integrity sha512-+soJL85Xnsak/VOdxSgiDKhhaFiOkckiswwrXdiWVCxV3LP9POyJR4AqGVFGdkntJ3YT63mtwTYuunFeId+XSA== + dependencies: + classnames "^2.2.6" + gitdiff-parser "^0.1.2" + leven "^2.1.0" + lodash.escape "^4.0.1" + lodash.findlastindex "^4.6.0" + lodash.mapvalues "^4.6.0" + warning "^4.0.1" + react-docgen-typescript@^1.2.3: version "1.12.2" resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-1.12.2.tgz#d5fb578d12f6876efdde69176f4ea658e75a9a29" @@ -16011,6 +16049,13 @@ unicode-property-aliases-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" +unidiff@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/unidiff/-/unidiff-0.0.4.tgz#257fc346ac6f134b0b75d08895320872a737e222" + integrity sha1-JX/DRqxvE0sLddCIlTIIcqc34iI= + dependencies: + diff "^2.2.2" + unified@^6.0.0: version "6.2.0" resolved "https://registry.yarnpkg.com/unified/-/unified-6.2.0.tgz#7fbd630f719126d67d40c644b7e3f617035f6dba" @@ -16619,6 +16664,13 @@ warning@^3.0.0: dependencies: loose-envify "^1.0.0" +warning@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607" + integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug== + dependencies: + loose-envify "^1.0.0" + watch@~0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.10.0.tgz#77798b2da0f9910d595f1ace5b0c2258521f21dc"