From 15ab8a4901dc00ddf76b214ddda8f7ca4f050fc1 Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Thu, 11 Nov 2021 16:36:21 +0530 Subject: [PATCH] Feat : show diff between the two versions in versioning history. (#1107) * Feat : show diff of two version in versioning history. * Style: changed text and background color for diff. * removed previous version dependency * added support for description diff. * minor fix * minor fix * added support for tags diff. * fixed markdown parsing issue. * minor change for entityTable * reverting the entitytable changes. * markdown parsing fixed * added support for column changed diff. * fixed column name column style * fixed versioning summary formatting. * Refactored :EntityversionPage Code * addressing review comments. * eslint-disabled check --- .../src/main/resources/ui/package-lock.json | 5 + .../src/main/resources/ui/package.json | 1 + .../DatasetVersion.component.tsx | 300 ++++++++++++++++-- .../EntityInfoDrawer.style.css | 2 + .../EntityTable/EntityTable.component.tsx | 187 ++++++----- .../EntityVersionTimeLine.tsx | 20 +- .../SchemaTab/SchemaTab.component.tsx | 34 +- .../EntityVersionPage.component.tsx | 163 ++++------ .../main/resources/ui/src/react-app-env.d.ts | 1 + .../src/main/resources/ui/src/styles/temp.css | 13 + .../ui/src/utils/EntityVersionUtils.tsx | 195 ++++++++++++ 11 files changed, 692 insertions(+), 229 deletions(-) create mode 100644 catalog-rest-service/src/main/resources/ui/src/utils/EntityVersionUtils.tsx diff --git a/catalog-rest-service/src/main/resources/ui/package-lock.json b/catalog-rest-service/src/main/resources/ui/package-lock.json index c377164b880..8617aea8bb7 100644 --- a/catalog-rest-service/src/main/resources/ui/package-lock.json +++ b/catalog-rest-service/src/main/resources/ui/package-lock.json @@ -7058,6 +7058,11 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" + }, "diff-match-patch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", diff --git a/catalog-rest-service/src/main/resources/ui/package.json b/catalog-rest-service/src/main/resources/ui/package.json index a957b29a982..936029b6b0d 100644 --- a/catalog-rest-service/src/main/resources/ui/package.json +++ b/catalog-rest-service/src/main/resources/ui/package.json @@ -26,6 +26,7 @@ "codemirror": "^5.62.3", "cookie-storage": "^6.1.0", "core-js": "^3.10.1", + "diff": "^5.0.0", "draft-js": "^0.11.7", "eslint": "^6.6.0", "eslint-config-react-app": "^5.2.1", diff --git a/catalog-rest-service/src/main/resources/ui/src/components/DatasetVersion/DatasetVersion.component.tsx b/catalog-rest-service/src/main/resources/ui/src/components/DatasetVersion/DatasetVersion.component.tsx index 2d372e2832e..b0ca3c27713 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/DatasetVersion/DatasetVersion.component.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/DatasetVersion/DatasetVersion.component.tsx @@ -1,8 +1,21 @@ import classNames from 'classnames'; -import React from 'react'; -import { getTeamDetailsPath } from '../../constants/constants'; -import { ColumnJoins } from '../../generated/entity/data/table'; +import { cloneDeep, isEqual, isUndefined } from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { + ChangeDescription, + Column, + ColumnJoins, + Table, +} from '../../generated/entity/data/table'; +import { TagLabel } from '../../generated/type/tagLabel'; import { getPartialNameFromFQN } from '../../utils/CommonUtils'; +import { + getDescriptionDiff, + getDiffByFieldName, + getDiffValue, + getTagsDiff, +} from '../../utils/EntityVersionUtils'; +import { getOwnerFromId } from '../../utils/TableUtils'; import { getTableTags } from '../../utils/TagsUtils'; import Description from '../common/description/Description'; import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo'; @@ -25,19 +38,262 @@ const DatasetVersion: React.FC = ({ backHandler, versionHandler, }: DatasetVersionProp) => { - const extraInfo = [ - { - key: 'Owner', - value: - owner?.type === 'team' - ? getTeamDetailsPath(owner?.name || '') - : owner?.name || '', - placeholderText: owner?.displayName || '', - isLink: owner?.type === 'team', - openInNewTab: false, - }, - { key: 'Tier', value: tier ? tier.split('.')[1] : '' }, - ]; + const [changeDescription, setChangeDescription] = useState( + currentVersionData.changeDescription as ChangeDescription + ); + + const getChangeColName = (name: string | undefined) => { + return name?.split('.')?.slice(-2, -1)[0]; + }; + + const isEndsWithField = (name: string | undefined, checkWith: string) => { + return name?.endsWith(checkWith); + }; + + const getExtraInfo = () => { + const ownerDiff = getDiffByFieldName('owner', changeDescription); + + const oldOwner = getOwnerFromId( + JSON.parse( + ownerDiff?.added?.oldValue ?? + ownerDiff?.deleted?.oldValue ?? + ownerDiff?.updated?.oldValue ?? + '{}' + )?.id + ); + const newOwner = getOwnerFromId( + JSON.parse( + ownerDiff?.added?.newValue ?? + ownerDiff?.deleted?.newValue ?? + ownerDiff?.updated?.newValue ?? + '{}' + )?.id + ); + const ownerPlaceHolder = owner?.name ?? owner?.displayName ?? ''; + + const tagsDiff = getDiffByFieldName('tags', changeDescription, true); + const newTier = [ + ...JSON.parse( + tagsDiff?.added?.newValue ?? + tagsDiff?.deleted?.newValue ?? + tagsDiff?.updated?.newValue ?? + '[]' + ), + ].find((t) => (t?.tagFQN as string).startsWith('Tier')); + + const oldTier = [ + ...JSON.parse( + tagsDiff?.added?.oldValue ?? + tagsDiff?.deleted?.oldValue ?? + tagsDiff?.updated?.oldValue ?? + '[]' + ), + ].find((t) => (t?.tagFQN as string).startsWith('Tier')); + + const extraInfo = [ + { + key: 'Owner', + value: + !isUndefined(ownerDiff.added) || + !isUndefined(ownerDiff.deleted) || + !isUndefined(ownerDiff.updated) + ? getDiffValue( + oldOwner?.displayName || oldOwner?.name || '', + newOwner?.displayName || newOwner?.name || '' + ) + : ownerPlaceHolder + ? getDiffValue(ownerPlaceHolder, ownerPlaceHolder) + : '', + }, + { + key: 'Tier', + value: + !isUndefined(newTier) || !isUndefined(oldTier) + ? getDiffValue( + oldTier?.tagFQN?.split('.')[1] || '', + newTier?.tagFQN?.split('.')[1] || '' + ) + : tier + ? tier.split('.')[1] + : '', + }, + ]; + + return extraInfo; + }; + + const getTableDescription = () => { + const descriptionDiff = getDiffByFieldName( + 'description', + changeDescription + ); + const oldDescription = + descriptionDiff?.added?.oldValue ?? + descriptionDiff?.deleted?.oldValue ?? + descriptionDiff?.updated?.oldValue; + const newDescription = + descriptionDiff?.added?.newValue ?? + descriptionDiff?.deleted?.newValue ?? + descriptionDiff?.updated?.newValue; + + return getDescriptionDiff( + oldDescription, + newDescription, + currentVersionData.description + ); + }; + + const updatedColumns = (): Table['columns'] => { + const colList = cloneDeep(currentVersionData.columns); + const columnsDiff = getDiffByFieldName('columns', changeDescription); + const changedColName = getChangeColName( + columnsDiff?.added?.name ?? + columnsDiff?.deleted?.name ?? + columnsDiff?.updated?.name + ); + + if ( + isEndsWithField( + columnsDiff?.added?.name ?? + columnsDiff?.deleted?.name ?? + columnsDiff?.updated?.name, + 'description' + ) + ) { + const oldDescription = + columnsDiff?.added?.oldValue ?? + columnsDiff?.deleted?.oldValue ?? + columnsDiff?.updated?.oldValue; + const newDescription = + columnsDiff?.added?.newValue ?? + columnsDiff?.deleted?.newValue ?? + columnsDiff?.updated?.newValue; + + const formatColumnData = (arr: Table['columns']) => { + arr?.forEach((i) => { + if (isEqual(i.name, changedColName)) { + i.description = getDescriptionDiff( + oldDescription, + newDescription, + i.description + ); + } else { + formatColumnData(i?.children as Table['columns']); + } + }); + }; + + formatColumnData(colList); + + return colList; + } else if ( + isEndsWithField( + columnsDiff?.added?.name ?? + columnsDiff?.deleted?.name ?? + columnsDiff?.updated?.name, + 'tags' + ) + ) { + const oldTags: Array = JSON.parse( + columnsDiff?.added?.oldValue ?? + columnsDiff?.deleted?.oldValue ?? + columnsDiff?.updated?.oldValue ?? + '[]' + ); + const newTags: Array = JSON.parse( + columnsDiff?.added?.newValue ?? + columnsDiff?.deleted?.newValue ?? + columnsDiff?.updated?.newValue ?? + '[]' + ); + + const formatColumnData = (arr: Table['columns']) => { + arr?.forEach((i) => { + if (isEqual(i.name, changedColName)) { + const flag: { [x: string]: boolean } = {}; + const uniqueTags: Array< + TagLabel & { added: boolean; removed: boolean } + > = []; + const tagsDiff = getTagsDiff(oldTags, newTags); + [...tagsDiff, ...(i.tags as Array)].forEach( + (elem: TagLabel & { added: boolean; removed: boolean }) => { + if (!flag[elem.tagFQN as string]) { + flag[elem.tagFQN as string] = true; + uniqueTags.push(elem); + } + } + ); + i.tags = uniqueTags; + } else { + formatColumnData(i?.children as Table['columns']); + } + }); + }; + + formatColumnData(colList); + + return colList; + } else { + const columnsDiff = getDiffByFieldName( + 'columns', + changeDescription, + true + ); + if (columnsDiff.added) { + const newCol: Array = JSON.parse( + columnsDiff.added?.newValue ?? '[]' + ); + newCol.forEach((col) => { + const formatColumnData = (arr: Table['columns']) => { + arr?.forEach((i) => { + if (isEqual(i.name, col.name)) { + i.tags = col.tags?.map((tag) => ({ ...tag, added: true })); + i.description = getDescriptionDiff( + undefined, + col.description, + col.description + ); + i.dataTypeDisplay = getDescriptionDiff( + undefined, + col.dataTypeDisplay, + col.dataTypeDisplay + ); + i.name = getDescriptionDiff(undefined, col.name, col.name); + } else { + formatColumnData(i?.children as Table['columns']); + } + }); + }; + formatColumnData(colList); + }); + + return colList; + } else if (columnsDiff.deleted) { + const newCol: Array = JSON.parse( + columnsDiff.deleted?.oldValue ?? '[]' + ); + const newColumns = newCol.map((col) => ({ + ...col, + tags: col.tags?.map((tag) => ({ ...tag, removed: true })), + description: getDescriptionDiff( + col.description, + undefined, + col.description + ), + dataTypeDisplay: getDescriptionDiff( + col.dataTypeDisplay, + undefined, + col.dataTypeDisplay + ), + name: getDescriptionDiff(col.name, undefined, col.name), + })); + + return [...newColumns, ...colList]; + } else { + return colList; + } + } + }; const tabs = [ { @@ -52,6 +308,12 @@ const DatasetVersion: React.FC = ({ }, ]; + useEffect(() => { + setChangeDescription( + currentVersionData.changeDescription as ChangeDescription + ); + }, [currentVersionData]); + return (
= ({ = ({
@@ -92,7 +354,7 @@ const DatasetVersion: React.FC = ({ ['column'], '.' )} - columns={currentVersionData.columns} + columns={updatedColumns()} joins={currentVersionData.joins as ColumnJoins[]} />
diff --git a/catalog-rest-service/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.style.css b/catalog-rest-service/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.style.css index 951f002f582..9c9bcfc05a5 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.style.css +++ b/catalog-rest-service/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.style.css @@ -10,10 +10,12 @@ overflow-y: auto; padding: 16px; transform: translateX(100%); + display: none; border-left: 1px solid #d9ceee; transition: transform 0.3s ease-out; } .side-drawer.open { transform: translateX(0); + display: block; } diff --git a/catalog-rest-service/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx b/catalog-rest-service/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx index 3462d85b25c..e423c27403d 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx @@ -454,76 +454,109 @@ const EntityTable = ({ ) : null} {cell.column.id === 'dataTypeDisplay' && ( -
- {cell.value.length > 25 ? ( - - - -
- } - position="bottom" - theme="light" - trigger="click"> -

- -

- - + <> + {isReadOnly ? ( +
+ +
) : ( - cell.value.toLowerCase() + <> + {cell.value.length > 25 ? ( + + + + + } + position="bottom" + theme="light" + trigger="click"> +
+ +
+
+
+ ) : ( + cell.value.toLowerCase() + )} + )} - + )} {cell.column.id === 'tags' && ( -
{ - if (!editColumnTag && !isReadOnly) { - handleEditColumnTag(row.original, row.id); - } - }}> - - { - handleTagSelection(); - }} - onSelectionChange={(tags) => { - handleTagSelection(tags); - }}> - {!isReadOnly ? ( - cell.value.length ? ( - - ) : ( - - - + <> + {isReadOnly ? ( +
+ {cell.value?.map( + ( + tag: TagLabel & { + added: boolean; + removed: boolean; + }, + i: number + ) => ( + ) - ) : null} - - -
+ )} +
+ ) : ( +
{ + if (!editColumnTag) { + handleEditColumnTag(row.original, row.id); + } + }}> + + { + handleTagSelection(); + }} + onSelectionChange={(tags) => { + handleTagSelection(tags); + }}> + {cell.value.length ? ( + + ) : ( + + + + )} + + +
+ )} + )} {cell.column.id === 'description' && (
@@ -647,15 +680,23 @@ const EntityTable = ({
)} {cell.column.id === 'name' && ( - - {getConstraintIcon(row.original.constraint)} - {cell.render('Cell')} - + <> + {isReadOnly ? ( +
+ +
+ ) : ( + + {getConstraintIcon(row.original.constraint)} + {cell.render('Cell')} + + )} + )} ); diff --git a/catalog-rest-service/src/main/resources/ui/src/components/EntityVersionTimeLine/EntityVersionTimeLine.tsx b/catalog-rest-service/src/main/resources/ui/src/components/EntityVersionTimeLine/EntityVersionTimeLine.tsx index 45c9b7e82f6..9107d134552 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/EntityVersionTimeLine/EntityVersionTimeLine.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/EntityVersionTimeLine/EntityVersionTimeLine.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import { toString } from 'lodash'; import React, { Fragment } from 'react'; import { EntityHistory } from '../../generated/type/entityHistory'; +import { getSummary } from '../../utils/EntityVersionUtils'; import './EntityVersionTimeLine.css'; type Props = { @@ -55,24 +56,7 @@ const EntityVersionTimeLine: React.FC = ({ v{parseFloat(currV?.version).toFixed(1)}

- {currV?.changeDescription?.fieldsAdded?.length ? ( -

- {currV?.changeDescription?.fieldsAdded?.join(',')} has been - added -

- ) : null} - {currV?.changeDescription?.fieldsUpdated?.length ? ( -

- {currV?.changeDescription?.fieldsUpdated?.join(',')} has - been updated -

- ) : null} - {currV?.changeDescription?.fieldsDeleted?.length ? ( -

- {currV?.changeDescription?.fieldsDeleted?.join(',')} has - been deleted -

- ) : null} + {getSummary(currV?.changeDescription)}

{currV?.updatedBy} diff --git a/catalog-rest-service/src/main/resources/ui/src/components/SchemaTab/SchemaTab.component.tsx b/catalog-rest-service/src/main/resources/ui/src/components/SchemaTab/SchemaTab.component.tsx index 35213f8357f..62ad5c0a193 100644 --- a/catalog-rest-service/src/main/resources/ui/src/components/SchemaTab/SchemaTab.component.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/components/SchemaTab/SchemaTab.component.tsx @@ -127,22 +127,24 @@ const SchemaTab: FunctionComponent = ({ ) : null}

-
- {checkedValue === 'schema' ? ( - - ) : ( - - )} -
+ {columns?.length > 0 ? ( +
+ {checkedValue === 'schema' ? ( + + ) : ( + + )} +
+ ) : null}
); diff --git a/catalog-rest-service/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx b/catalog-rest-service/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx index 89dea92b9d9..b68ef1e9216 100644 --- a/catalog-rest-service/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx +++ b/catalog-rest-service/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx @@ -1,6 +1,5 @@ import { AxiosError, AxiosResponse } from 'axios'; -import { toString } from 'lodash'; -import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; +import React, { FunctionComponent, useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { getDatabase } from '../../axiosAPIs/databaseAPI'; import { getServiceById } from '../../axiosAPIs/serviceAPI'; @@ -34,7 +33,7 @@ const EntityVersionPage: FunctionComponent = () => { const [currentVersionData, setCurrentVersionData] = useState( {} as Table ); - const [latestVersion, setLatestVersion] = useState(); + const { version, datasetFQN } = useParams() as Record; const [isLoading, setIsloading] = useState(false); const [versionList, setVersionList] = useState( @@ -45,8 +44,6 @@ const EntityVersionPage: FunctionComponent = () => { TitleBreadcrumbProps['titleLinks'] >([]); - const isMounted = useRef(false); - const backHandler = () => { history.push(getDatasetDetailsPath(datasetFQN)); }; @@ -62,11 +59,10 @@ const EntityVersionPage: FunctionComponent = () => { ['owner', 'tags', 'database'] ) .then((res: AxiosResponse) => { - const { id, version, owner, tags, name, database } = res.data; + const { id, owner, tags, name, database } = res.data; setTier(getTierFromTableTags(tags)); setOwner(getOwnerFromId(owner?.id)); setCurrentVersionData(res.data); - setLatestVersion(version); getDatabase(database.id, 'service').then((resDB: AxiosResponse) => { getServiceById('databaseServices', resDB.data.service?.id).then( (resService: AxiosResponse) => { @@ -119,100 +115,67 @@ const EntityVersionPage: FunctionComponent = () => { }; const fetchCurrentVersion = () => { - if (toString(latestVersion) === version) { - setIsVersionLoading(true); - getTableDetailsByFQN( - getPartialNameFromFQN( - datasetFQN, - ['service', 'database', 'table'], - '.' - ), - ['owner', 'tags'] - ) - .then((vRes: AxiosResponse) => { - const { owner, tags } = vRes.data; - setTier(getTierFromTableTags(tags)); - setOwner(getOwnerFromId(owner?.id)); - setCurrentVersionData(vRes.data); - setIsVersionLoading(false); - }) - .catch((err: AxiosError) => { - const msg = err.message; - showToast({ - variant: 'error', - body: - msg ?? `Error while fetching ${datasetFQN} version ${version}`, - }); + setIsVersionLoading(true); + getTableDetailsByFQN( + getPartialNameFromFQN(datasetFQN, ['service', 'database', 'table'], '.'), + 'database' + ) + .then((res: AxiosResponse) => { + const { id, database, name } = res.data; + getDatabase(database.id, 'service').then((resDB: AxiosResponse) => { + getServiceById('databaseServices', resDB.data.service?.id).then( + (resService: AxiosResponse) => { + setSlashedTableName([ + { + name: resService.data.name, + url: resService.data.name + ? getServiceDetailsPath( + resService.data.name, + resService.data.serviceType + ) + : '', + imgSrc: resService.data.serviceType + ? serviceTypeLogo(resService.data.serviceType) + : undefined, + }, + { + name: resDB.data.name, + url: getDatabaseDetailsPath(resDB.data.fullyQualifiedName), + }, + { + name: name, + url: '', + activeTitle: true, + }, + ]); + } + ); }); - } else { - setIsVersionLoading(true); - getTableDetailsByFQN( - getPartialNameFromFQN( - datasetFQN, - ['service', 'database', 'table'], - '.' - ), - 'database' - ) - .then((res: AxiosResponse) => { - const { id, database, name } = res.data; - getDatabase(database.id, 'service').then((resDB: AxiosResponse) => { - getServiceById('databaseServices', resDB.data.service?.id).then( - (resService: AxiosResponse) => { - setSlashedTableName([ - { - name: resService.data.name, - url: resService.data.name - ? getServiceDetailsPath( - resService.data.name, - resService.data.serviceType - ) - : '', - imgSrc: resService.data.serviceType - ? serviceTypeLogo(resService.data.serviceType) - : undefined, - }, - { - name: resDB.data.name, - url: getDatabaseDetailsPath(resDB.data.fullyQualifiedName), - }, - { - name: name, - url: '', - activeTitle: true, - }, - ]); - } - ); - }); - getTableVersion(id, version) - .then((vRes: AxiosResponse) => { - const { owner, tags } = vRes.data; - setTier(getTierFromTableTags(tags)); - setOwner(getOwnerFromId(owner?.id)); - setCurrentVersionData(vRes.data); - setIsVersionLoading(false); - }) - .catch((err: AxiosError) => { - const msg = err.message; - showToast({ - variant: 'error', - body: - msg ?? - `Error while fetching ${datasetFQN} version ${version}`, - }); + getTableVersion(id, version) + .then((vRes: AxiosResponse) => { + const { owner, tags } = vRes.data; + setTier(getTierFromTableTags(tags)); + setOwner(getOwnerFromId(owner?.id)); + setCurrentVersionData(vRes.data); + setIsVersionLoading(false); + }) + .catch((err: AxiosError) => { + const msg = err.message; + showToast({ + variant: 'error', + body: + msg ?? `Error while fetching ${datasetFQN} version ${version}`, }); - }) - .catch((err: AxiosError) => { - const msg = err.message; - showToast({ - variant: 'error', - body: - msg ?? `Error while fetching ${datasetFQN} version ${version}`, }); + }) + .catch((err: AxiosError) => { + const msg = err.message; + showToast({ + variant: 'error', + body: msg ?? `Error while fetching ${datasetFQN} version ${version}`, }); - } + }); }; useEffect(() => { @@ -220,15 +183,9 @@ const EntityVersionPage: FunctionComponent = () => { }, [datasetFQN]); useEffect(() => { - if (isMounted.current) { - fetchCurrentVersion(); - } + fetchCurrentVersion(); }, [version]); - useEffect(() => { - isMounted.current = true; - }, []); - return ( <> {isLoading ? ( diff --git a/catalog-rest-service/src/main/resources/ui/src/react-app-env.d.ts b/catalog-rest-service/src/main/resources/ui/src/react-app-env.d.ts index be5a819a675..b717e9bd4a8 100644 --- a/catalog-rest-service/src/main/resources/ui/src/react-app-env.d.ts +++ b/catalog-rest-service/src/main/resources/ui/src/react-app-env.d.ts @@ -30,3 +30,4 @@ declare module 'slick-carousel'; declare module 'react-table'; declare module 'recharts'; declare module 'react-tutorial'; +declare module 'diff'; diff --git a/catalog-rest-service/src/main/resources/ui/src/styles/temp.css b/catalog-rest-service/src/main/resources/ui/src/styles/temp.css index cb28eb3134f..e95a7693dd0 100644 --- a/catalog-rest-service/src/main/resources/ui/src/styles/temp.css +++ b/catalog-rest-service/src/main/resources/ui/src/styles/temp.css @@ -690,3 +690,16 @@ body .public-DraftStyleDefault-ul li::marker { body .list-option.rdw-option-active { box-shadow: none; } + +/* Diff style */ + +.diff-added { + background: rgba(0, 131, 118, 0.2); + color: #008376; + width: fit-content; +} +.diff-removed { + color: #008376; + text-decoration: line-through; + width: fit-content; +} diff --git a/catalog-rest-service/src/main/resources/ui/src/utils/EntityVersionUtils.tsx b/catalog-rest-service/src/main/resources/ui/src/utils/EntityVersionUtils.tsx new file mode 100644 index 00000000000..65459229481 --- /dev/null +++ b/catalog-rest-service/src/main/resources/ui/src/utils/EntityVersionUtils.tsx @@ -0,0 +1,195 @@ +import classNames from 'classnames'; +import { diffArrays, diffWordsWithSpace } from 'diff'; +import { isUndefined } from 'lodash'; +import React, { Fragment } from 'react'; +import ReactDOMServer from 'react-dom/server'; +import ReactMarkdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import gfm from 'remark-gfm'; +import { + ChangeDescription, + FieldChange, +} from '../generated/entity/services/databaseService'; +import { TagLabel } from '../generated/type/tagLabel'; +import { isValidJSONString } from './StringsUtils'; + +/* eslint-disable */ +const parseMarkdown = ( + content: string, + className: string, + isNewLine: boolean +) => { + return ( + ') + .replaceAll('\\', '')} + components={{ + h1: 'p', + h2: 'p', + h3: 'p', + h4: 'p', + h5: 'p', + h6: 'p', + p: ({ node, children, ...props }) => { + return ( + <> + {isNewLine ? ( +

+ {children} +

+ ) : ( + + {children} + + )} + + ); + }, + ul: ({ node, children, ...props }) => { + const { ordered: _ordered, ...rest } = props; + + return ( +
    + {children} +
+ ); + }, + }} + rehypePlugins={[[rehypeRaw, { allowDangerousHtml: false }]]} + remarkPlugins={[gfm]} + /> + ); +}; + +export const getDiffByFieldName = ( + name: string, + changeDescription: ChangeDescription, + exactMatch?: boolean +): { + added: FieldChange | undefined; + deleted: FieldChange | undefined; + updated: FieldChange | undefined; +} => { + const fieldsAdded = changeDescription?.fieldsAdded || []; + const fieldsDeleted = changeDescription?.fieldsDeleted || []; + const fieldsUpdated = changeDescription?.fieldsUpdated || []; + if (exactMatch) { + return { + added: fieldsAdded.find((ch) => ch.name === name), + deleted: fieldsDeleted.find((ch) => ch.name === name), + updated: fieldsUpdated.find((ch) => ch.name === name), + }; + } else { + return { + added: fieldsAdded.find((ch) => ch.name?.startsWith(name)), + deleted: fieldsDeleted.find((ch) => ch.name?.startsWith(name)), + updated: fieldsUpdated.find((ch) => ch.name?.startsWith(name)), + }; + } +}; + +export const getDiffValue = (oldValue: string, newValue: string) => { + const diff = diffWordsWithSpace(oldValue, newValue); + + return diff.map((part: any, index: any) => { + return ( + + {part.value} + + ); + }); +}; + +export const getDescriptionDiff = ( + oldDescription: string | undefined, + newDescription: string | undefined, + latestDescription: string | undefined +) => { + if (!isUndefined(newDescription) || !isUndefined(oldDescription)) { + const diff = diffWordsWithSpace(oldDescription ?? '', newDescription ?? ''); + + const result: Array = diff.map((part: any, index: any) => { + const classes = classNames( + { 'diff-added': part.added }, + { 'diff-removed': part.removed } + ); + + return ReactDOMServer.renderToString( + + {parseMarkdown( + part.value, + classes, + part.value?.startsWith('\n\n') || part.value?.includes('\n\n') + )} + + ); + }); + + return result.join(''); + } else { + return latestDescription || ''; + } +}; + +export const getTagsDiff = ( + oldTagList: Array, + newTagList: Array +) => { + const tagDiff = diffArrays(oldTagList, newTagList); + const result = tagDiff + + .map((part: any) => + (part.value as Array).map((tag) => ({ + ...tag, + added: part.added, + removed: part.removed, + })) + ) + ?.flat(Infinity); + + return result; +}; + +export const summaryFormatter = (v: FieldChange) => { + const value = JSON.parse( + isValidJSONString(v?.newValue) + ? v?.newValue + : isValidJSONString(v?.oldValue) + ? v?.oldValue + : '{}' + ); + if (v.name === 'columns') { + return `columns ${value?.map((val: any) => val?.name).join(',')}`; + } else if (v.name === 'tags' || v.name?.endsWith('tags')) { + return `tags ${value?.map((val: any) => val?.tagFQN)?.join(',')}`; + } else { + return v.name; + } +}; + +export const getSummary = (changeDescription: ChangeDescription) => { + const fieldsAdded = [...(changeDescription?.fieldsAdded || [])]; + const fieldsDeleted = [...(changeDescription?.fieldsDeleted || [])]; + const fieldsUpdated = [...(changeDescription?.fieldsUpdated || [])]; + + return ( + + {fieldsAdded?.length > 0 ? ( +

{fieldsAdded?.map(summaryFormatter)} has been added

+ ) : null} + {fieldsUpdated?.length ? ( +

{fieldsUpdated?.map(summaryFormatter)} has been updated

+ ) : null} + {fieldsDeleted?.length ? ( +

{fieldsDeleted?.map(summaryFormatter)} has been deleted

+ ) : null} +
+ ); +};