mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-01 18:15:50 +00:00
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:
parent
6c9202db3d
commit
15ab8a4901
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -30,3 +30,4 @@ declare module 'slick-carousel';
|
||||
declare module 'react-table';
|
||||
declare module 'recharts';
|
||||
declare module 'react-tutorial';
|
||||
declare module 'diff';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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(/</g, '<')
|
||||
.replaceAll(/>/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>
|
||||
);
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user