@reltio/sources 1.4.1585 → 1.4.1586-mui5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +1 -0
- package/package.json +38 -21
- package/public/bundle.js +205 -0
- package/public/bundle.js.LICENSE.txt +79 -0
- package/public/package.json +22 -0
- package/scripts/build/index.js +20 -0
- package/src/components/CrosswalkTreeView/CrosswalkTreeView.tsx +47 -0
- package/src/components/CrosswalkTreeView/__tests__/CrosswalkTreeView.spec.tsx +72 -0
- package/src/components/CrosswalkTreeView/__tests__/helpers.spec.ts +961 -0
- package/src/components/CrosswalkTreeView/helpers.ts +91 -0
- package/src/components/CrosswalkTreeView/styles.ts +10 -0
- package/src/components/EmptyState/EmptyState.tsx +20 -0
- package/src/components/EmptyState/__tests__/EmptyState.test.tsx +13 -0
- package/src/components/EmptyState/icons/EmptySources.svg +43 -0
- package/src/components/EmptyState/styles.ts +19 -0
- package/src/components/LegendTable/LegendTable.tsx +124 -0
- package/src/components/LegendTable/LegendTableRow/ContributorRow/ContributorRow.tsx +46 -0
- package/src/components/LegendTable/LegendTableRow/ContributorRow/__tests__/ContributorRow.test.tsx +103 -0
- package/src/components/LegendTable/LegendTableRow/ContributorRow/styles.ts +31 -0
- package/src/components/LegendTable/LegendTableRow/LegendTableRow.tsx +179 -0
- package/src/components/LegendTable/LegendTableRow/__tests__/LegendTableRow.test.tsx +371 -0
- package/src/components/LegendTable/LegendTableRow/styles.ts +31 -0
- package/src/components/LegendTable/__tests__/LegendTable.test.tsx +273 -0
- package/src/components/LegendTable/__tests__/helpers.test.ts +541 -0
- package/src/components/LegendTable/helpers.ts +64 -0
- package/src/components/LegendTable/styles.ts +17 -0
- package/src/components/LegendView/LegendView.tsx +201 -0
- package/src/components/LegendView/__tests__/LegendView.test.tsx +307 -0
- package/src/components/LegendView/styles.ts +51 -0
- package/src/components/RightSidePanel/RightSidePanel.tsx +25 -0
- package/src/components/RightSidePanel/__tests__/RightSidePanel.test.tsx +28 -0
- package/src/components/RightSidePanel/styles.ts +14 -0
- package/src/components/RulesestsSelector/RulesestsSelector.tsx +31 -0
- package/src/components/RulesestsSelector/RulesetOption/RulesetOption.tsx +25 -0
- package/src/components/RulesestsSelector/RulesetOption/__tests__/RulesetOption.test.tsx +25 -0
- package/src/components/RulesestsSelector/RulesetOption/styles.ts +27 -0
- package/src/components/RulesestsSelector/RulesetsMenuList/RulesetsMenuList.tsx +14 -0
- package/src/components/RulesestsSelector/__tests__/RulesetsSelector.test.tsx +91 -0
- package/src/components/RulesestsSelector/styles.ts +7 -0
- package/src/components/SankeyChart/Link.tsx +31 -0
- package/src/components/SankeyChart/MergeTooltip.tsx +37 -0
- package/src/components/SankeyChart/Node.tsx +137 -0
- package/src/components/SankeyChart/SankeyChart.tsx +103 -0
- package/src/components/SankeyChart/__tests__/Link.spec.tsx +127 -0
- package/src/components/SankeyChart/__tests__/MergeTooltip.spec.tsx +22 -0
- package/src/components/SankeyChart/__tests__/Node.spec.tsx +248 -0
- package/src/components/SankeyChart/__tests__/SankeyChart.spec.tsx +161 -0
- package/src/components/SankeyChart/__tests__/__snapshots__/SankeyChart.spec.tsx.snap +183 -0
- package/src/components/SankeyChart/sankey.ts +322 -0
- package/src/components/SankeyChart/styles.ts +41 -0
- package/src/components/SankeyChart/useSankey.tsx +44 -0
- package/src/components/SelectableTree/SelectableTree.tsx +46 -0
- package/src/components/SelectableTree/__tests__/SelectableTree.test.tsx +28 -0
- package/src/components/SelectableTree/__tests__/__snapshots__/SelectableTree.test.tsx.snap +59 -0
- package/src/components/SelectableTree/styles.ts +42 -0
- package/src/components/SourcesView/SourcesView.tsx +318 -0
- package/src/components/SourcesView/__tests__/IntegrationSourcesView.spec.tsx +199 -0
- package/src/components/SourcesView/__tests__/SourcesView.spec.tsx +903 -0
- package/src/components/SourcesView/__tests__/data/metadata.data.ts +27 -0
- package/src/components/SourcesView/__tests__/data/store.data.ts +65 -0
- package/src/components/SourcesView/icons/LegendIcon.svg +8 -0
- package/src/components/SourcesView/index.tsx +57 -0
- package/src/components/SourcesView/styles.ts +44 -0
- package/src/components/TreeActionSnackbar/TreeActionSnackbar.tsx +38 -0
- package/src/components/TreeActionSnackbar/__tests__/TreeActionSnackbar.test.tsx +38 -0
- package/src/components/TreeActionSnackbar/styles.ts +11 -0
- package/src/components/UnmergeDialog/UnmergeDialog.tsx +78 -0
- package/src/components/UnmergeDialog/__tests__/UnmergeDialog.test.tsx +124 -0
- package/src/components/UnmergeDialog/__tests__/testData.data.ts +271 -0
- package/src/components/UnmergeDialog/styles.ts +34 -0
- package/src/components/UnsubscribeDialog/UnsubscribeDialog.tsx +64 -0
- package/src/components/UnsubscribeDialog/__tests__/UnsubscribeDialog.test.tsx +59 -0
- package/src/components/UnsubscribeDialog/styles.ts +31 -0
- package/src/hooks/__tests__/useCrosswalkTree.specs.tsx +68 -0
- package/src/hooks/__tests__/useCrosswalkTreeActions.specs.ts +110 -0
- package/src/hooks/__tests__/useCrosswalksDisplay.specs.tsx +213 -0
- package/src/hooks/__tests__/useEntityWithSurvivorshipGroups.specs.ts +160 -0
- package/src/hooks/useCrosswalkTree.ts +38 -0
- package/src/hooks/useCrosswalkTreeActions.ts +83 -0
- package/src/hooks/useCrosswalksDisplay.ts +110 -0
- package/src/hooks/useEntityWithSurvivorshipGroups.tsx +51 -0
- package/src/icons/notMatch.svg +14 -0
- package/src/index.ts +1 -0
- package/src/services/errors.ts +11 -0
- package/src/types/LegendMode.ts +5 -0
- package/src/types/SankeyChartData.ts +31 -0
- package/src/types/SourcesSavedState.ts +3 -0
- package/src/types/UnmergeContributorEvent.ts +7 -0
- package/src/types/UnsubscribeContributorEvent.ts +6 -0
- package/src/types/index.ts +4 -0
- package/tsconfig.json +4 -0
- package/webpack.config.js +10 -0
- package/bundle.js +0 -2
- package/bundle.js.LICENSE.txt +0 -42
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {shallow} from 'enzyme';
|
|
3
|
+
import MenuItem from '@mui/material/MenuItem';
|
|
4
|
+
import {RulesetOption} from '../RulesetOption';
|
|
5
|
+
|
|
6
|
+
describe('RulesetOption tests', () => {
|
|
7
|
+
it('should render all parts correctly', () => {
|
|
8
|
+
const innerRef = React.createRef<HTMLLIElement>();
|
|
9
|
+
const innerProps = {onClick: jest.fn()};
|
|
10
|
+
const data = {
|
|
11
|
+
uri: 'uri1',
|
|
12
|
+
mapping: [],
|
|
13
|
+
label: 'some label',
|
|
14
|
+
description: 'some description'
|
|
15
|
+
};
|
|
16
|
+
const component = shallow(<RulesetOption innerRef={innerRef} innerProps={innerProps} data={data} />);
|
|
17
|
+
const menuItemComponent = component.find(MenuItem);
|
|
18
|
+
expect(menuItemComponent.props()).toMatchObject({
|
|
19
|
+
className: 'root',
|
|
20
|
+
onClick: innerProps.onClick
|
|
21
|
+
});
|
|
22
|
+
expect(menuItemComponent.find('.label').text()).toBe(data.label);
|
|
23
|
+
expect(menuItemComponent.find('.description').text()).toBe(data.description);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {makeStyles} from '@mui/styles';
|
|
2
|
+
|
|
3
|
+
export const useStyles = makeStyles((theme) => ({
|
|
4
|
+
root: {
|
|
5
|
+
display: 'flex',
|
|
6
|
+
flexDirection: 'column',
|
|
7
|
+
alignItems: 'flex-start',
|
|
8
|
+
paddingTop: '14px',
|
|
9
|
+
paddingBottom: '13px',
|
|
10
|
+
borderBottom: '1px solid rgba(0, 0, 0, 0.05)',
|
|
11
|
+
whiteSpace: 'normal',
|
|
12
|
+
'&:last-child': {
|
|
13
|
+
borderBottom: 'none'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
label: {
|
|
17
|
+
marginBottom: '2px',
|
|
18
|
+
fontSize: '16px',
|
|
19
|
+
lineHeight: '24px',
|
|
20
|
+
color: theme.palette.text.primary
|
|
21
|
+
},
|
|
22
|
+
description: {
|
|
23
|
+
fontSize: '13px',
|
|
24
|
+
lineHeight: '15px',
|
|
25
|
+
color: theme.palette.text.secondary
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {MenuListComponentProps} from 'react-select';
|
|
3
|
+
|
|
4
|
+
export const RulesetsMenuList = <OptionType, IsMulti extends boolean>(
|
|
5
|
+
props: MenuListComponentProps<OptionType, IsMulti>
|
|
6
|
+
) => {
|
|
7
|
+
const {innerRef, getStyles, children} = props;
|
|
8
|
+
const styles = {...getStyles('menuList', props), maxHeight: '500px', padding: 0};
|
|
9
|
+
return (
|
|
10
|
+
<div ref={innerRef} style={styles}>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {act} from 'react-dom/test-utils';
|
|
3
|
+
import {shallow} from 'enzyme';
|
|
4
|
+
import {DropDownSelector} from '@reltio/components';
|
|
5
|
+
import {RulesetsSelector} from '../RulesestsSelector';
|
|
6
|
+
import {RulesetOption} from '../RulesetOption/RulesetOption';
|
|
7
|
+
import {RulesetsMenuList} from '../RulesetsMenuList/RulesetsMenuList';
|
|
8
|
+
|
|
9
|
+
describe('RulesetsSelector tests', () => {
|
|
10
|
+
const survivorshipGroups = [
|
|
11
|
+
{
|
|
12
|
+
uri: 'uri1',
|
|
13
|
+
mapping: [],
|
|
14
|
+
label: 'label 1',
|
|
15
|
+
description: 'description 1'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
uri: 'uri2',
|
|
19
|
+
mapping: [],
|
|
20
|
+
label: 'label 2'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
uri: 'uri3',
|
|
24
|
+
mapping: []
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
it('should render RulesetsSelector with correct props', () => {
|
|
29
|
+
const survivorshipGroupUri = 'uri3';
|
|
30
|
+
const onChange = jest.fn();
|
|
31
|
+
|
|
32
|
+
const component = shallow(
|
|
33
|
+
<RulesetsSelector
|
|
34
|
+
survivorshipGroups={survivorshipGroups}
|
|
35
|
+
currentSurvivorshipGroupUri={survivorshipGroupUri}
|
|
36
|
+
onChange={onChange}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(component.find(DropDownSelector).props()).toEqual({
|
|
41
|
+
height: 40,
|
|
42
|
+
className: 'root',
|
|
43
|
+
components: {Option: RulesetOption, MenuList: RulesetsMenuList},
|
|
44
|
+
label: 'Rulesets',
|
|
45
|
+
options: [
|
|
46
|
+
{
|
|
47
|
+
description: 'description 1',
|
|
48
|
+
label: 'label 1',
|
|
49
|
+
mapping: [],
|
|
50
|
+
uri: 'uri1'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'label 2',
|
|
54
|
+
mapping: [],
|
|
55
|
+
uri: 'uri2'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: '<No label>',
|
|
59
|
+
mapping: [],
|
|
60
|
+
uri: 'uri3'
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
value: {
|
|
64
|
+
label: '<No label>',
|
|
65
|
+
mapping: [],
|
|
66
|
+
uri: 'uri3'
|
|
67
|
+
},
|
|
68
|
+
onChange: expect.any(Function)
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('onChange handler should work correctly', () => {
|
|
73
|
+
const survivorshipGroupUri = 'uri1';
|
|
74
|
+
const onChange = jest.fn();
|
|
75
|
+
|
|
76
|
+
const component = shallow(
|
|
77
|
+
<RulesetsSelector
|
|
78
|
+
survivorshipGroups={survivorshipGroups}
|
|
79
|
+
currentSurvivorshipGroupUri={survivorshipGroupUri}
|
|
80
|
+
onChange={onChange}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
act(() => {
|
|
85
|
+
const survivorshipGroup = {label: 'label 2', mapping: [], uri: 'uri2'};
|
|
86
|
+
component.find(DropDownSelector).prop('onChange')(survivorshipGroup);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(onChange).toHaveBeenCalledWith('uri2');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {useCrosswalkColor, useCrosswalkHighlight} from '@reltio/components';
|
|
3
|
+
import {SankeyLink} from '../../types/SankeyChartData';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
link: SankeyLink;
|
|
7
|
+
path: string;
|
|
8
|
+
highlightEnabled: boolean;
|
|
9
|
+
isColored: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const Link = ({link, path, highlightEnabled, isColored}: Props) => {
|
|
13
|
+
const {crosswalk} = link;
|
|
14
|
+
const color = useCrosswalkColor(crosswalk);
|
|
15
|
+
const {isDimmed, highlightCrosswalk, removeHighlight} = useCrosswalkHighlight(crosswalk);
|
|
16
|
+
return (
|
|
17
|
+
<path
|
|
18
|
+
d={path}
|
|
19
|
+
fill={isColored ? (isDimmed ? 'rgba(0, 0, 0, 0.12)' : color) : '#F0F0F0'}
|
|
20
|
+
opacity={highlightEnabled && isDimmed ? 0.19 : 1}
|
|
21
|
+
onMouseEnter={() => {
|
|
22
|
+
if (highlightEnabled) highlightCrosswalk();
|
|
23
|
+
}}
|
|
24
|
+
onMouseLeave={() => {
|
|
25
|
+
if (highlightEnabled) removeHighlight();
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default Link;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import i18n from 'ui-i18n';
|
|
3
|
+
import {LightArrowTooltip} from '@reltio/components';
|
|
4
|
+
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
|
5
|
+
import PersonIcon from '@mui/icons-material/Person';
|
|
6
|
+
import {useStyles} from './styles';
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
user: string;
|
|
10
|
+
time: number;
|
|
11
|
+
children: React.ReactElement;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const MergeTooltip = ({user, time, children}: Props) => {
|
|
15
|
+
const styles = useStyles();
|
|
16
|
+
const tooltipTitle = (
|
|
17
|
+
<div>
|
|
18
|
+
<span className={styles.tooltipTitle}>{i18n.text('Merge details')}</span>
|
|
19
|
+
<div className={styles.tooltipRow}>
|
|
20
|
+
<CalendarTodayIcon className={styles.tooltipIcon} />
|
|
21
|
+
{i18n.date(time, 'L LTS')}
|
|
22
|
+
</div>
|
|
23
|
+
<div className={styles.tooltipRow}>
|
|
24
|
+
<PersonIcon className={styles.tooltipIcon} />
|
|
25
|
+
{user}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<LightArrowTooltip title={tooltipTitle} placement={'top'}>
|
|
32
|
+
{children}
|
|
33
|
+
</LightArrowTooltip>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default MergeTooltip;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, {MouseEvent, useEffect, useRef, useState} from 'react';
|
|
2
|
+
import i18n from 'ui-i18n';
|
|
3
|
+
import Popover from '@mui/material/Popover';
|
|
4
|
+
import MergeTooltip from './MergeTooltip';
|
|
5
|
+
import {SankeyNode} from '../../types/SankeyChartData';
|
|
6
|
+
import {SmallIconButtonWithTooltip, noop, useCrosswalkColor, useCrosswalkHighlight} from '@reltio/components';
|
|
7
|
+
import {groupBy, prop, pipe, map, head, last, max, min} from 'ramda';
|
|
8
|
+
import UnmergeIcon from '../../icons/notMatch.svg';
|
|
9
|
+
import {useStyles} from './styles';
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
nodeWidth: number;
|
|
13
|
+
node: SankeyNode;
|
|
14
|
+
highlightEnabled: boolean;
|
|
15
|
+
isColored: boolean;
|
|
16
|
+
disableUnmerge: boolean;
|
|
17
|
+
onUnmerge: (contributorUri: string) => void;
|
|
18
|
+
isDragging: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Node = ({node, nodeWidth, highlightEnabled, isColored, disableUnmerge, onUnmerge, isDragging}: Props) => {
|
|
22
|
+
const styles = useStyles();
|
|
23
|
+
const [labelX, setLabelX] = useState(0);
|
|
24
|
+
const [anchorEl, setAnchorEl] = useState(null);
|
|
25
|
+
const textRef = useRef(null);
|
|
26
|
+
const {
|
|
27
|
+
x,
|
|
28
|
+
y,
|
|
29
|
+
name,
|
|
30
|
+
label,
|
|
31
|
+
targetLinks,
|
|
32
|
+
sourceLinks,
|
|
33
|
+
entity: {crosswalks = [], merges}
|
|
34
|
+
} = node;
|
|
35
|
+
const [firstCrosswalk] = crosswalks;
|
|
36
|
+
const {isDimmed, highlightCrosswalk, removeHighlight} = useCrosswalkHighlight(firstCrosswalk);
|
|
37
|
+
const color = useCrosswalkColor(firstCrosswalk);
|
|
38
|
+
const startNodeHeight = crosswalks?.length
|
|
39
|
+
? sourceLinks[crosswalks.length - 1].sy + sourceLinks[crosswalks.length - 1].dy
|
|
40
|
+
: 0;
|
|
41
|
+
|
|
42
|
+
const renderMergeNodes = () => {
|
|
43
|
+
const timeMap = pipe(map(prop('source')), groupBy(prop('time')))(targetLinks);
|
|
44
|
+
|
|
45
|
+
return merges?.map(({time, user}, index) => {
|
|
46
|
+
const sources = timeMap[time];
|
|
47
|
+
if (!sources) return;
|
|
48
|
+
|
|
49
|
+
const [start, end] = sources.reduce(
|
|
50
|
+
(acc, {sourceLinks}) => [
|
|
51
|
+
min(head(sourceLinks).ty, acc[0]),
|
|
52
|
+
max(last(sourceLinks).ty + last(sourceLinks).dy, acc[1])
|
|
53
|
+
],
|
|
54
|
+
[Infinity, 0]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<MergeTooltip user={user} time={time} key={'node-' + index}>
|
|
59
|
+
<rect
|
|
60
|
+
transform={`translate(0,${start})`}
|
|
61
|
+
height={end - start}
|
|
62
|
+
width={nodeWidth}
|
|
63
|
+
className={styles.node}
|
|
64
|
+
fillOpacity={0.54}
|
|
65
|
+
fill={'#F0F0F0'}
|
|
66
|
+
opacity={highlightEnabled && isDimmed ? 0.38 : 1}
|
|
67
|
+
/>
|
|
68
|
+
</MergeTooltip>
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const openPopup = (e: MouseEvent<SVGRectElement>) => setAnchorEl(e.currentTarget);
|
|
74
|
+
const closePopup = () => setAnchorEl(null);
|
|
75
|
+
const handleUnmerge = () => {
|
|
76
|
+
onUnmerge(name);
|
|
77
|
+
closePopup();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const nodeHeight = node.dy;
|
|
82
|
+
const textWidth = textRef?.current?.clientWidth || 0;
|
|
83
|
+
if (nodeHeight < textWidth) setLabelX(nodeHeight / 2 - textWidth / 2);
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (isDragging && !!anchorEl) closePopup();
|
|
88
|
+
}, [isDragging]);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<g transform={`translate(${x},${y})`}>
|
|
92
|
+
{name === 'ROOT' ? (
|
|
93
|
+
<text dy={-5} dx={labelX} ref={textRef} className={styles.rootNode}>
|
|
94
|
+
{label || ''}
|
|
95
|
+
</text>
|
|
96
|
+
) : (
|
|
97
|
+
<>
|
|
98
|
+
<rect
|
|
99
|
+
height={startNodeHeight}
|
|
100
|
+
width={14}
|
|
101
|
+
className={styles.node}
|
|
102
|
+
fill={isColored ? (isDimmed ? 'rgba(0, 0, 0, 0.12)' : color) : '#F0F0F0'}
|
|
103
|
+
key={'node-main'}
|
|
104
|
+
opacity={highlightEnabled && isDimmed ? 0.19 : 1}
|
|
105
|
+
onMouseEnter={() => {
|
|
106
|
+
if (highlightEnabled) highlightCrosswalk();
|
|
107
|
+
}}
|
|
108
|
+
onMouseLeave={() => {
|
|
109
|
+
if (highlightEnabled) removeHighlight();
|
|
110
|
+
}}
|
|
111
|
+
onClick={disableUnmerge ? noop : openPopup}
|
|
112
|
+
cursor={disableUnmerge ? 'default' : 'pointer'}
|
|
113
|
+
data-reltio-id="reltio-sankey-chart-node"
|
|
114
|
+
/>
|
|
115
|
+
<Popover
|
|
116
|
+
open={!!anchorEl}
|
|
117
|
+
anchorEl={anchorEl}
|
|
118
|
+
anchorOrigin={{vertical: 'top', horizontal: 'right'}}
|
|
119
|
+
onClose={closePopup}
|
|
120
|
+
classes={{paper: styles.popupContainer}}
|
|
121
|
+
data-reltio-id="reltio-unmerge-popup"
|
|
122
|
+
>
|
|
123
|
+
<SmallIconButtonWithTooltip
|
|
124
|
+
size="XS"
|
|
125
|
+
tooltipTitle={i18n.text('Unmerge')}
|
|
126
|
+
icon={UnmergeIcon}
|
|
127
|
+
onClick={handleUnmerge}
|
|
128
|
+
/>
|
|
129
|
+
</Popover>
|
|
130
|
+
{!!targetLinks?.length && renderMergeNodes()}
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</g>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export default Node;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, {useMemo, useState, useEffect} from 'react';
|
|
2
|
+
import useSankey from './useSankey';
|
|
3
|
+
import Node from './Node';
|
|
4
|
+
import Link from './Link';
|
|
5
|
+
import {SankeyData} from '../../types/SankeyChartData';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
data: SankeyData;
|
|
11
|
+
accentContributors?: boolean;
|
|
12
|
+
disableUnmerge: boolean;
|
|
13
|
+
onUnmerge: (contributorUri: string) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SankeyChart = ({width, height, data, accentContributors, onUnmerge, disableUnmerge}: Props) => {
|
|
17
|
+
const [isDragging, setDragging] = useState(false);
|
|
18
|
+
const [isMouseDown, setMouseDown] = useState(false);
|
|
19
|
+
const [previousPageX, setPreviousPageX] = useState(0);
|
|
20
|
+
const [offsetX, setOffsetX] = useState(0);
|
|
21
|
+
const {links, nodes, nodeWidth, path} = useSankey({data, width, height});
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setOffsetX(0);
|
|
25
|
+
setPreviousPageX(0);
|
|
26
|
+
}, [width, height]);
|
|
27
|
+
|
|
28
|
+
const sankeyNodes = useMemo(
|
|
29
|
+
() =>
|
|
30
|
+
nodes.map((node, index) => (
|
|
31
|
+
<Node
|
|
32
|
+
highlightEnabled={accentContributors}
|
|
33
|
+
isColored={accentContributors}
|
|
34
|
+
node={node}
|
|
35
|
+
nodeWidth={nodeWidth}
|
|
36
|
+
key={`SankeyNode-${index}`}
|
|
37
|
+
disableUnmerge={disableUnmerge}
|
|
38
|
+
onUnmerge={onUnmerge}
|
|
39
|
+
isDragging={isDragging}
|
|
40
|
+
/>
|
|
41
|
+
)),
|
|
42
|
+
[nodes, accentContributors, nodeWidth, width, height, isDragging]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const sankeyLinks = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
links.map((link, index) => (
|
|
48
|
+
<Link
|
|
49
|
+
highlightEnabled={!accentContributors}
|
|
50
|
+
isColored={!accentContributors}
|
|
51
|
+
link={link}
|
|
52
|
+
path={path(link)}
|
|
53
|
+
key={`SankeyLink-${index}`}
|
|
54
|
+
/>
|
|
55
|
+
)),
|
|
56
|
+
[accentContributors, links, path]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const extractPositionDelta = (event: React.MouseEvent) => {
|
|
60
|
+
const left = event.pageX;
|
|
61
|
+
const offsetX = left - previousPageX;
|
|
62
|
+
setPreviousPageX(left);
|
|
63
|
+
return offsetX;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const onDown = (event: React.MouseEvent) => {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
setMouseDown(true);
|
|
69
|
+
extractPositionDelta(event);
|
|
70
|
+
};
|
|
71
|
+
const onMove = (event: React.MouseEvent) => {
|
|
72
|
+
if (!isMouseDown) return;
|
|
73
|
+
setDragging(true);
|
|
74
|
+
const left = extractPositionDelta(event);
|
|
75
|
+
setOffsetX(offsetX + left);
|
|
76
|
+
};
|
|
77
|
+
const onUp = () => {
|
|
78
|
+
setMouseDown(false);
|
|
79
|
+
setDragging(false);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const pointerEvents = isDragging ? 'none' : 'auto';
|
|
83
|
+
const cursor = isDragging ? 'move' : 'auto';
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<svg
|
|
87
|
+
width={width}
|
|
88
|
+
height={height}
|
|
89
|
+
cursor={cursor}
|
|
90
|
+
onMouseDown={onDown}
|
|
91
|
+
onMouseMove={onMove}
|
|
92
|
+
onMouseUp={onUp}
|
|
93
|
+
onMouseLeave={onUp}
|
|
94
|
+
>
|
|
95
|
+
<g transform={`translate(${offsetX},${height - 1}) rotate(-90)`} pointerEvents={pointerEvents}>
|
|
96
|
+
<g>{sankeyLinks}</g>
|
|
97
|
+
<g>{sankeyNodes}</g>
|
|
98
|
+
</g>
|
|
99
|
+
</svg>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export default SankeyChart;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import {render} from '@testing-library/react';
|
|
4
|
+
import {Link} from '../Link';
|
|
5
|
+
import * as reltioComponents from '@reltio/components';
|
|
6
|
+
|
|
7
|
+
jest.mock('@reltio/components', () => ({
|
|
8
|
+
...jest.requireActual<Record<string, unknown>>('@reltio/components'),
|
|
9
|
+
useCrosswalkColor: jest.fn(),
|
|
10
|
+
useCrosswalkHighlight: jest.fn()
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe('Sankey link', () => {
|
|
14
|
+
const highlightCrosswalk = jest.fn();
|
|
15
|
+
const removeHighlight = jest.fn();
|
|
16
|
+
let user;
|
|
17
|
+
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
jest.spyOn(reltioComponents, 'useCrosswalkColor').mockReturnValue('#FF0000');
|
|
20
|
+
jest.spyOn(reltioComponents, 'useCrosswalkHighlight').mockReturnValue({
|
|
21
|
+
isHighlighted: true,
|
|
22
|
+
isDimmed: false,
|
|
23
|
+
highlightCrosswalk,
|
|
24
|
+
removeHighlight
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
user = userEvent.setup({delay: null});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const props = {
|
|
33
|
+
link: {
|
|
34
|
+
value: 10,
|
|
35
|
+
source: {
|
|
36
|
+
entity: {uri: 'testUri'},
|
|
37
|
+
name: 'test',
|
|
38
|
+
time: 1234
|
|
39
|
+
},
|
|
40
|
+
target: {
|
|
41
|
+
entity: {uri: 'test1Uri'},
|
|
42
|
+
name: 'test1',
|
|
43
|
+
time: 1234
|
|
44
|
+
},
|
|
45
|
+
crosswalk: {type: 'testType', uri: 'crosswalkUri', value: '20'}
|
|
46
|
+
},
|
|
47
|
+
path: 'M10 10 H 90 V 90 H 10 L 10 10',
|
|
48
|
+
highlightEnabled: false,
|
|
49
|
+
isColored: false
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
it('should render Link correctly', () => {
|
|
53
|
+
const {asFragment} = render(<Link {...props} />);
|
|
54
|
+
expect(asFragment()).toMatchInlineSnapshot(`
|
|
55
|
+
<DocumentFragment>
|
|
56
|
+
<path
|
|
57
|
+
d="M10 10 H 90 V 90 H 10 L 10 10"
|
|
58
|
+
fill="#F0F0F0"
|
|
59
|
+
opacity="1"
|
|
60
|
+
/>
|
|
61
|
+
</DocumentFragment>
|
|
62
|
+
`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should not work highlighting if highlightEnabled = false', async () => {
|
|
66
|
+
const {container} = render(<Link {...props} />);
|
|
67
|
+
const path = container.querySelector('path');
|
|
68
|
+
await user.hover(path);
|
|
69
|
+
expect(highlightCrosswalk).toBeCalledTimes(0);
|
|
70
|
+
await user.unhover(path);
|
|
71
|
+
expect(removeHighlight).toBeCalledTimes(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should work highlighting if highlightEnabled = true', async () => {
|
|
75
|
+
const {container} = render(<Link {...props} highlightEnabled={true} />);
|
|
76
|
+
const path = container.querySelector('path');
|
|
77
|
+
await user.hover(path);
|
|
78
|
+
expect(highlightCrosswalk).toBeCalledTimes(1);
|
|
79
|
+
await user.unhover(path);
|
|
80
|
+
expect(removeHighlight).toBeCalledTimes(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should not color link if isColored = false', () => {
|
|
84
|
+
const {asFragment} = render(<Link {...props} isColored={false} />);
|
|
85
|
+
expect(asFragment()).toMatchInlineSnapshot(`
|
|
86
|
+
<DocumentFragment>
|
|
87
|
+
<path
|
|
88
|
+
d="M10 10 H 90 V 90 H 10 L 10 10"
|
|
89
|
+
fill="#F0F0F0"
|
|
90
|
+
opacity="1"
|
|
91
|
+
/>
|
|
92
|
+
</DocumentFragment>
|
|
93
|
+
`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should not color link if isColored = true', () => {
|
|
97
|
+
const {asFragment} = render(<Link {...props} isColored={true} />);
|
|
98
|
+
expect(asFragment()).toMatchInlineSnapshot(`
|
|
99
|
+
<DocumentFragment>
|
|
100
|
+
<path
|
|
101
|
+
d="M10 10 H 90 V 90 H 10 L 10 10"
|
|
102
|
+
fill="#FF0000"
|
|
103
|
+
opacity="1"
|
|
104
|
+
/>
|
|
105
|
+
</DocumentFragment>
|
|
106
|
+
`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should be dimmed if isDimmed and highlightCrosswalk = true', () => {
|
|
110
|
+
jest.spyOn(reltioComponents, 'useCrosswalkHighlight').mockReturnValue({
|
|
111
|
+
isHighlighted: true,
|
|
112
|
+
isDimmed: true,
|
|
113
|
+
highlightCrosswalk,
|
|
114
|
+
removeHighlight
|
|
115
|
+
});
|
|
116
|
+
const {asFragment} = render(<Link {...props} highlightEnabled={true} isColored={true} />);
|
|
117
|
+
expect(asFragment()).toMatchInlineSnapshot(`
|
|
118
|
+
<DocumentFragment>
|
|
119
|
+
<path
|
|
120
|
+
d="M10 10 H 90 V 90 H 10 L 10 10"
|
|
121
|
+
fill="rgba(0, 0, 0, 0.12)"
|
|
122
|
+
opacity="0.19"
|
|
123
|
+
/>
|
|
124
|
+
</DocumentFragment>
|
|
125
|
+
`);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {shallow} from 'enzyme';
|
|
3
|
+
import {LightArrowTooltip} from '@reltio/components';
|
|
4
|
+
import {MergeTooltip} from '../MergeTooltip';
|
|
5
|
+
|
|
6
|
+
describe('MergeTooltip', () => {
|
|
7
|
+
it('should render MergeTooltip', () => {
|
|
8
|
+
const wrapper = shallow(
|
|
9
|
+
<MergeTooltip user={'TestUser'} time={1620711408828}>
|
|
10
|
+
<span>Text</span>
|
|
11
|
+
</MergeTooltip>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(wrapper.find('span').text()).toBe('Text');
|
|
15
|
+
|
|
16
|
+
const title = wrapper.find(LightArrowTooltip).prop('title') as React.ReactElement;
|
|
17
|
+
const titleWrapper = shallow(title);
|
|
18
|
+
|
|
19
|
+
const rows = titleWrapper.find('.tooltipRow');
|
|
20
|
+
expect(rows.at(1).text()).toBe('TestUser');
|
|
21
|
+
});
|
|
22
|
+
});
|