Skip to content

Commit e93f951

Browse files
phixMephix
andauthored
Web: Updates for Table and Column Lineage (#2725)
* Initial commit with new rendering engine. * Adding prototype for column lineage view. * Updates for zoom controls. * Removing extra imports. * Extra meta information. * Adjusting application styles. * Improving code quality, splitting components. * Moving code around. * Saving partial progress. * Some progress on lineage view. * Checkpoint for tooltips. * Adding in side navigation. * Fixing links. * Fixing up lineage state. * Fixing column lineage. * Fixing up the back button. * Fixing tests. * Filtering out the non-null assertions. * More test fixes. * Code review comments and running formatter. * Adding full mode to graph. * Changing some language. * Minor update to fix search issue. * Removing Some text. * Improving fonts and layout in the action bar. * Table level node encoding. * Column Level changes. * Fixing reload on column lineage and adjusting colors. --------- Co-authored-by: phix <[email protected]>
1 parent b111d64 commit e93f951

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+8518
-1453
lines changed

web/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
/coverage
1313
/dist
1414
/node_modules
15+
/libs/graph/node_modules
1516
npm-debug.log*
1617
yarn-debug.log*
1718
yarn-error.log*

web/libs/graph/README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# graph
2+
3+
`graph` is a library for generating and rendering graphs. It uses elkjs to generate layouts and renders them using a custom renderer.
4+
5+
## Installing/Using
6+
7+
To use the library, add the following to your package.json:
8+
9+
```json
10+
{
11+
"dependencies": {
12+
"elkjs": "^0.8.2"
13+
}
14+
}
15+
```
16+
17+
You also need to use the webpack CopyPlugin to copy the elk-worker file. Add the following to your webpack config:
18+
19+
```js
20+
const CopyPlugin = require('copy-webpack-plugin');
21+
const path = require('path');
22+
23+
// look for elkjs package folder
24+
const elkjsRoot = path.dirname(require.resolve('elkjs/package.json'));
25+
26+
// add the CopyPlugin to the webpack config
27+
plugins: [
28+
...
29+
new CopyPlugin({
30+
patterns: [
31+
{ from: path.join(elkjsRoot, 'lib/elk-worker.min.js'), to: 'elk-worker.min.js' },
32+
],
33+
}),
34+
]
35+
```
36+
37+
## useLayout
38+
39+
`useLayout` provides a common interface for creating layouts with [ElkJs](https://github.com/kieler/elkjs).
40+
41+
```ts
42+
import { useLayout } from 'graph'
43+
44+
const { nodes: positionedNodes, edges: positionedEdges } = useLayout<'myKind', MyNodeDataType>({
45+
id,
46+
nodes,
47+
edges,
48+
direction: 'right',
49+
webWorkerUrl,
50+
getLayoutOptions
51+
});
52+
```
53+
54+
The layout calculations are asynchronous. Once the layout is complete, the returned `nodes` will each include
55+
a `bottomLeftCorner: {x: number: y: number }` property, along with all the original properties.
56+
57+
## ZoomPanSvg Component
58+
59+
// To Add
60+
61+
## Graph Component
62+
63+
The Graph component is used to render a Graph. A `TaskNode` with run status is included, but custom ones are supported.
64+
65+
```tsx
66+
import { Graph, Edge, Node } from 'graph';
67+
68+
import { MyNodeComponent, MyNodeDataType, NodeRendererMap } from './';
69+
70+
const myNodesRenderers: NodeRendererMap<'myNode', MyNodeDataType> = new Map().set(
71+
'myNode',
72+
MyNodeComponent,
73+
);
74+
75+
// declare the nodes and edges
76+
const nodes: Node<'myNode', MyNodeDataType>[] = [
77+
{
78+
id: '1',
79+
label: 'Task 1',
80+
type: 'myNode',
81+
data: {
82+
// any additional data you want to store
83+
},
84+
},
85+
{
86+
id: '2',
87+
label: 'Task 2',
88+
type: 'myNode',
89+
data: {
90+
// any additional data you want to store
91+
},
92+
},
93+
];
94+
95+
const edges: Edge[] = [
96+
{
97+
id: '1',
98+
source: '1',
99+
target: '2',
100+
type: 'elbow',
101+
},
102+
];
103+
104+
// create a graph
105+
<Graph<'myNode', MyNodeDataType>
106+
nodes={nodes}
107+
edges={edges}
108+
direction="right"
109+
nodeRenderers={myNodesRenderers}
110+
/>;
111+
```

web/libs/graph/jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
...config,
3+
setupFilesAfterEnv: ['./jest.setup.js'],
4+
}

web/libs/graph/jest.setup.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* eslint-disable no-undef */
2+
import '@testing-library/jest-dom'
3+
4+
// Establish API mocking before all tests.
5+
beforeAll(() => {
6+
Object.defineProperty(window, 'matchMedia', {
7+
writable: true,
8+
value: jest.fn().mockImplementation((query) => ({
9+
matches: false,
10+
media: query,
11+
onchange: null,
12+
addListener: jest.fn(), // deprecated
13+
removeListener: jest.fn(), // deprecated
14+
addEventListener: jest.fn(),
15+
removeEventListener: jest.fn(),
16+
dispatchEvent: jest.fn(),
17+
})),
18+
})
19+
})

web/libs/graph/package.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "graph",
3+
"version": "0.0.1",
4+
"private": true,
5+
"main": "src/index.ts",
6+
"scripts": {
7+
"lint": "cd .. && yarn lint",
8+
"test": "jest",
9+
"test:silent": "yarn test --silent",
10+
"test:ci": "yarn test:silent --coverage --runInBand",
11+
"test:watch": "yarn test --watch",
12+
"tsc:validate": "tsc --noEmit"
13+
},
14+
"dependencies": {
15+
"@react-hook/size": "^2.1.2",
16+
"lodash": "^4.17.21"
17+
},
18+
"peerDependencies": {
19+
"@chakra-ui/react": "^2",
20+
"elkjs": "^0.8.2",
21+
"react": "^18",
22+
"react-dom": "^18",
23+
"react-router-dom": "^6"
24+
},
25+
"devDependencies": {
26+
"@testing-library/jest-dom": "^5.16.5",
27+
"@testing-library/react": "^14.0.0",
28+
"@testing-library/react-hooks": "^8.0.1",
29+
"@testing-library/user-event": "^14.4.3",
30+
"@types/jest": "^27.5.2",
31+
"@types/react": "^18.0.15",
32+
"@types/react-dom": "^18.0.6",
33+
"d3-interpolate": "^2.0.1",
34+
"d3-selection": "^2.0.0",
35+
"d3-transition": "^2.0.0",
36+
"d3-zoom": "^2.0.0",
37+
"elkjs": "^0.8.2",
38+
"jest": "^27.5.1",
39+
"prettier": "^2.8.8",
40+
"react": "^18.2.0",
41+
"react-dom": "^18.2.0",
42+
"react-router-dom": "^6.3.0",
43+
"typescript": "^5.1.3",
44+
"web-worker": "^1.2.0"
45+
}
46+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react'
2+
3+
import { ElbowEdge } from './ElbowEdge'
4+
import { StraightEdge } from './StraightEdge'
5+
import type { PositionedEdge } from '../../types'
6+
7+
export interface EdgeProps {
8+
edge: PositionedEdge
9+
isMiniMap?: boolean
10+
}
11+
12+
export const Edge = (props: EdgeProps) => {
13+
const { edge } = props
14+
switch (edge.type) {
15+
case 'straight':
16+
return <StraightEdge {...props} />
17+
default:
18+
return <ElbowEdge {...props} />
19+
}
20+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react'
2+
3+
import { grey } from '@mui/material/colors'
4+
import type { ElkLabel } from 'elkjs'
5+
6+
interface Props {
7+
label?: ElkLabel
8+
endPointY?: number
9+
}
10+
11+
export const EdgeLabel = ({ label, endPointY }: Props) => {
12+
const labelColor = grey['400']
13+
14+
if (!label || !label.y || !label.x) return null
15+
16+
let { y } = label
17+
// The edge and label are rendering a little differently,
18+
// so we need some extra magic numbers to work right
19+
if (endPointY) y = label.y - 5 >= endPointY ? endPointY + 25 : endPointY - 15
20+
21+
return (
22+
<text fill={labelColor} x={label.x} y={y}>
23+
{label.text}
24+
</text>
25+
)
26+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { useMemo } from 'react'
2+
3+
import { chakra, keyframes, usePrefersReducedMotion } from '@chakra-ui/react'
4+
5+
import { EdgeLabel } from './EdgeLabel'
6+
import { grey } from '@mui/material/colors'
7+
import type { EdgeProps } from './Edge'
8+
9+
const ChakraPolyline = chakra('polyline') // need to use animation prop
10+
const marchingAnts = keyframes({ from: { strokeDashoffset: 60 }, to: { strokeDashoffset: 0 } })
11+
12+
export const ElbowEdge = ({ edge, isMiniMap }: EdgeProps) => {
13+
const reducedMotion = usePrefersReducedMotion() || isMiniMap // do not animate the minimap
14+
15+
const points = useMemo(() => {
16+
const { startPoint, bendPoints, endPoint } = edge
17+
18+
return [
19+
// source
20+
{ x: startPoint.x, y: startPoint.y },
21+
...(bendPoints?.map((bendPoint) => ({ x: bendPoint.x, y: bendPoint.y })) ?? []),
22+
// target
23+
{ x: endPoint.x, y: endPoint.y },
24+
]
25+
}, [edge])
26+
27+
// Find the longest edge that the label would be near
28+
let longestEdge: { y: number; length: number } | undefined
29+
if (edge.label) {
30+
points.forEach((p, i) => {
31+
if (i > 0) {
32+
const length = p.x - points[i - 1].x
33+
if (!longestEdge || longestEdge.length < length) longestEdge = { y: p.y, length }
34+
}
35+
})
36+
}
37+
return (
38+
<>
39+
<polyline
40+
id={`${edge.sourceNodeId}-${edge.targetNodeId}`}
41+
fill='none'
42+
stroke={edge.color || grey['600']}
43+
strokeWidth={edge.strokeWidth || 2}
44+
strokeLinejoin='round'
45+
points={points.map(({ x, y }) => `${x},${y}`).join(' ')}
46+
/>
47+
<EdgeLabel label={edge.label} endPointY={longestEdge?.y} />
48+
{!reducedMotion && edge.isAnimated && (
49+
<ChakraPolyline
50+
id={`${edge.sourceNodeId}-${edge.targetNodeId}-animated`}
51+
fill='none'
52+
strokeLinecap='round'
53+
stroke={edge.color || grey['600']}
54+
strokeWidth={edge.strokeWidth || 5}
55+
strokeLinejoin='round'
56+
strokeDasharray='0px 60px'
57+
animation={`${marchingAnts} infinite 2s linear`}
58+
points={points.map(({ x, y }) => `${x},${y}`).join(' ')}
59+
/>
60+
)}
61+
</>
62+
)
63+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from 'react'
2+
3+
import { chakra, keyframes, usePrefersReducedMotion } from '@chakra-ui/react'
4+
5+
import { EdgeLabel } from './EdgeLabel'
6+
import { grey } from '@mui/material/colors'
7+
import type { EdgeProps } from './Edge'
8+
9+
const ChakraLine = chakra('line') // need to use animation prop
10+
const marchingAnts = keyframes({ from: { strokeDashoffset: 60 }, to: { strokeDashoffset: 0 } })
11+
12+
export const StraightEdge = ({ edge, isMiniMap }: EdgeProps) => {
13+
const reducedMotion = usePrefersReducedMotion() || isMiniMap // do not animate the minimap
14+
const color = grey['600']
15+
16+
return (
17+
<>
18+
<line
19+
id={`${edge.sourceNodeId}-${edge.targetNodeId}`}
20+
fill='none'
21+
stroke={edge.color || color}
22+
strokeWidth={edge.strokeWidth || 2}
23+
strokeLinejoin='round'
24+
x1={edge.startPoint.x}
25+
y1={edge.startPoint.y}
26+
x2={edge.endPoint.x}
27+
y2={edge.endPoint.y}
28+
/>
29+
<EdgeLabel label={edge.label} endPointY={edge.endPoint.y} />
30+
{!reducedMotion && edge.isAnimated && (
31+
<ChakraLine
32+
id={`${edge.sourceNodeId}-${edge.targetNodeId}-animated`}
33+
fill='none'
34+
strokeLinecap='round'
35+
stroke={edge.color || color}
36+
strokeWidth={edge.strokeWidth || 5}
37+
strokeLinejoin='round'
38+
strokeDasharray='0px 60px'
39+
animation={`${marchingAnts} infinite 2s linear`}
40+
x1={edge.startPoint.x}
41+
y1={edge.startPoint.y}
42+
x2={edge.endPoint.x}
43+
y2={edge.endPoint.y}
44+
/>
45+
)}
46+
</>
47+
)
48+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Edge'

0 commit comments

Comments
 (0)