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 (
+
+ );
+ },
+ }}
+ 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}
+
+ );
+};