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
This commit is contained in:
Sachin Chaurasiya 2021-11-11 16:36:21 +05:30 committed by GitHub
parent 6c9202db3d
commit 15ab8a4901
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 692 additions and 229 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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<DatasetVersionProp> = ({
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<ChangeDescription>(
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<TagLabel> = JSON.parse(
columnsDiff?.added?.oldValue ??
columnsDiff?.deleted?.oldValue ??
columnsDiff?.updated?.oldValue ??
'[]'
);
const newTags: Array<TagLabel> = 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<TagLabel>)].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<Column> = 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<Column> = 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<DatasetVersionProp> = ({
},
];
useEffect(() => {
setChangeDescription(
currentVersionData.changeDescription as ChangeDescription
);
}, [currentVersionData]);
return (
<PageContainer>
<div
@ -65,7 +327,7 @@ const DatasetVersion: React.FC<DatasetVersionProp> = ({
<EntityPageInfo
isVersionSelected
entityName={currentVersionData.name}
extraInfo={extraInfo}
extraInfo={getExtraInfo()}
followersList={[]}
tags={getTableTags(currentVersionData.columns || [])}
tier={tier || ''}
@ -80,7 +342,7 @@ const DatasetVersion: React.FC<DatasetVersionProp> = ({
<div className="tw-col-span-full">
<Description
isReadOnly
description={currentVersionData.description || ''}
description={getTableDescription()}
/>
</div>
@ -92,7 +354,7 @@ const DatasetVersion: React.FC<DatasetVersionProp> = ({
['column'],
'.'
)}
columns={currentVersionData.columns}
columns={updatedColumns()}
joins={currentVersionData.joins as ColumnJoins[]}
/>
</div>

View File

@ -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;
}

View File

@ -454,76 +454,109 @@ const EntityTable = ({
) : null}
{cell.column.id === 'dataTypeDisplay' && (
<div>
{cell.value.length > 25 ? (
<span>
<PopOver
html={
<div className="tw-break-words">
<RichTextEditorPreviewer
markdown={cell.value.toLowerCase()}
/>
</div>
}
position="bottom"
theme="light"
trigger="click">
<p className="tw-cursor-pointer tw-underline tw-inline-block">
<RichTextEditorPreviewer
markdown={`${cell.value
.slice(0, 20)
.toLowerCase()}...`}
/>
</p>
</PopOver>
</span>
<>
{isReadOnly ? (
<div className="tw-flex tw-flex-wrap tw-w-60 tw-overflow-x-auto">
<RichTextEditorPreviewer
markdown={cell.value.toLowerCase()}
/>
</div>
) : (
cell.value.toLowerCase()
<>
{cell.value.length > 25 ? (
<span>
<PopOver
html={
<div className="tw-break-words">
<RichTextEditorPreviewer
markdown={cell.value.toLowerCase()}
/>
</div>
}
position="bottom"
theme="light"
trigger="click">
<div className="tw-cursor-pointer tw-underline tw-inline-block">
<RichTextEditorPreviewer
markdown={`${cell.value
.slice(0, 20)
.toLowerCase()}...`}
/>
</div>
</PopOver>
</span>
) : (
cell.value.toLowerCase()
)}
</>
)}
</div>
</>
)}
{cell.column.id === 'tags' && (
<div
onClick={() => {
if (!editColumnTag && !isReadOnly) {
handleEditColumnTag(row.original, row.id);
}
}}>
<NonAdminAction
html={getHtmlForNonAdminAction(Boolean(owner))}
isOwner={hasEditAccess}
position="left"
trigger="click">
<TagsContainer
editable={editColumnTag?.index === row.id}
selectedTags={cell.value || []}
tagList={allTags}
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags);
}}>
{!isReadOnly ? (
cell.value.length ? (
<button className="tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none">
<SVGIcons
alt="edit"
icon="icon-edit"
title="Edit"
width="10px"
/>
</button>
) : (
<span className="tw-opacity-60 group-hover:tw-opacity-100 tw-text-grey-muted group-hover:tw-text-primary">
<Tags tag="+ Add tag" type="outlined" />
</span>
<>
{isReadOnly ? (
<div className="tw-flex tw-flex-wrap">
{cell.value?.map(
(
tag: TagLabel & {
added: boolean;
removed: boolean;
},
i: number
) => (
<Tags
className={classNames(
{ 'diff-added': tag?.added },
{ 'diff-removed': tag?.removed }
)}
key={i}
tag={`#${tag.tagFQN}`}
/>
)
) : null}
</TagsContainer>
</NonAdminAction>
</div>
)}
</div>
) : (
<div
onClick={() => {
if (!editColumnTag) {
handleEditColumnTag(row.original, row.id);
}
}}>
<NonAdminAction
html={getHtmlForNonAdminAction(Boolean(owner))}
isOwner={hasEditAccess}
position="left"
trigger="click">
<TagsContainer
editable={editColumnTag?.index === row.id}
selectedTags={cell.value || []}
tagList={allTags}
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags);
}}>
{cell.value.length ? (
<button className="tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none">
<SVGIcons
alt="edit"
icon="icon-edit"
title="Edit"
width="10px"
/>
</button>
) : (
<span className="tw-opacity-60 group-hover:tw-opacity-100 tw-text-grey-muted group-hover:tw-text-primary">
<Tags tag="+ Add tag" type="outlined" />
</span>
)}
</TagsContainer>
</NonAdminAction>
</div>
)}
</>
)}
{cell.column.id === 'description' && (
<div>
@ -647,15 +680,23 @@ const EntityTable = ({
</div>
)}
{cell.column.id === 'name' && (
<span
style={{
paddingLeft: `${
row.canExpand ? '0px' : `${row.depth * 25}px`
}`,
}}>
{getConstraintIcon(row.original.constraint)}
{cell.render('Cell')}
</span>
<>
{isReadOnly ? (
<div className="tw-inline-block">
<RichTextEditorPreviewer markdown={cell.value} />
</div>
) : (
<span
style={{
paddingLeft: `${
row.canExpand ? '0px' : `${row.depth * 25}px`
}`,
}}>
{getConstraintIcon(row.original.constraint)}
{cell.render('Cell')}
</span>
)}
</>
)}
</td>
);

View File

@ -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<Props> = ({
v{parseFloat(currV?.version).toFixed(1)}
</p>
<div className="tw-text-xs tw-font-normal">
{currV?.changeDescription?.fieldsAdded?.length ? (
<p>
{currV?.changeDescription?.fieldsAdded?.join(',')} has been
added
</p>
) : null}
{currV?.changeDescription?.fieldsUpdated?.length ? (
<p>
{currV?.changeDescription?.fieldsUpdated?.join(',')} has
been updated
</p>
) : null}
{currV?.changeDescription?.fieldsDeleted?.length ? (
<p>
{currV?.changeDescription?.fieldsDeleted?.join(',')} has
been deleted
</p>
) : null}
{getSummary(currV?.changeDescription)}
</div>
<p className="tw-text-xs tw-italic">
<span className="tw-font-normal">{currV?.updatedBy}</span>

View File

@ -127,22 +127,24 @@ const SchemaTab: FunctionComponent<Props> = ({
) : null}
</div>
<div className="row">
<div className="col-sm-12">
{checkedValue === 'schema' ? (
<EntityTable
columnName={columnName}
hasEditAccess={Boolean(hasEditAccess)}
isReadOnly={isReadOnly}
joins={joins}
owner={owner}
searchText={lowerCase(searchText)}
tableColumns={columns}
onUpdate={onUpdate}
/>
) : (
<SampleDataTable sampleData={getSampleDataWithType()} />
)}
</div>
{columns?.length > 0 ? (
<div className="col-sm-12">
{checkedValue === 'schema' ? (
<EntityTable
columnName={columnName}
hasEditAccess={Boolean(hasEditAccess)}
isReadOnly={isReadOnly}
joins={joins}
owner={owner}
searchText={lowerCase(searchText)}
tableColumns={columns}
onUpdate={onUpdate}
/>
) : (
<SampleDataTable sampleData={getSampleDataWithType()} />
)}
</div>
) : null}
</div>
</div>
);

View File

@ -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<Table>(
{} as Table
);
const [latestVersion, setLatestVersion] = useState<string>();
const { version, datasetFQN } = useParams() as Record<string, string>;
const [isLoading, setIsloading] = useState<boolean>(false);
const [versionList, setVersionList] = useState<EntityHistory>(
@ -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 ? (

View File

@ -30,3 +30,4 @@ declare module 'slick-carousel';
declare module 'react-table';
declare module 'recharts';
declare module 'react-tutorial';
declare module 'diff';

View File

@ -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;
}

View File

@ -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 (
<ReactMarkdown
children={content
.replaceAll(/&lt;/g, '<')
.replaceAll(/&gt;/g, '>')
.replaceAll('\\', '')}
components={{
h1: 'p',
h2: 'p',
h3: 'p',
h4: 'p',
h5: 'p',
h6: 'p',
p: ({ node, children, ...props }) => {
return (
<>
{isNewLine ? (
<p className={className} {...props}>
{children}
</p>
) : (
<span className={className} {...props}>
{children}
</span>
)}
</>
);
},
ul: ({ node, children, ...props }) => {
const { ordered: _ordered, ...rest } = props;
return (
<ul className={className} style={{ marginLeft: '16px' }} {...rest}>
{children}
</ul>
);
},
}}
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 (
<span
className={classNames(
{ 'diff-added': part.added },
{ 'diff-removed': part.removed }
)}
key={index}>
{part.value}
</span>
);
});
};
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<string> = diff.map((part: any, index: any) => {
const classes = classNames(
{ 'diff-added': part.added },
{ 'diff-removed': part.removed }
);
return ReactDOMServer.renderToString(
<span key={index}>
{parseMarkdown(
part.value,
classes,
part.value?.startsWith('\n\n') || part.value?.includes('\n\n')
)}
</span>
);
});
return result.join('');
} else {
return latestDescription || '';
}
};
export const getTagsDiff = (
oldTagList: Array<TagLabel>,
newTagList: Array<TagLabel>
) => {
const tagDiff = diffArrays(oldTagList, newTagList);
const result = tagDiff
.map((part: any) =>
(part.value as Array<TagLabel>).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 (
<Fragment>
{fieldsAdded?.length > 0 ? (
<p>{fieldsAdded?.map(summaryFormatter)} has been added</p>
) : null}
{fieldsUpdated?.length ? (
<p>{fieldsUpdated?.map(summaryFormatter)} has been updated</p>
) : null}
{fieldsDeleted?.length ? (
<p>{fieldsDeleted?.map(summaryFormatter)} has been deleted</p>
) : null}
</Fragment>
);
};