Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 70 additions & 90 deletions web/src/__tests__/components/Lineage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,64 @@
// Copyright 2018-2023 contributors to the Marquez project
// SPDX-License-Identifier: Apache-2.0

import React from 'react'
import Lineage, { LineageProps, getSelectedPaths, initGraph, buildGraphAll } from '../../components/lineage/Lineage'
import { LineageNode } from '../../components/lineage/types'
import { render } from '@testing-library/react'
import { createBrowserHistory } from 'history'
import createSagaMiddleware from 'redux-saga'
import { createRouterMiddleware } from '@lagunovsky/redux-react-router'
import createRootReducer from '../../store/reducers'
import { composeWithDevTools } from '@redux-devtools/extension'
import { applyMiddleware, createStore } from 'redux'
import { Provider } from 'react-redux'
import { MqNode } from '../../components/lineage/types'
import { getSelectedPaths, initGraph, buildGraphAll } from '../../components/lineage/Lineage'
import { LineageNode, MqNode } from '../../components/lineage/types'
import { graphlib } from 'dagre'
import rootSaga from '../../store/sagas'

const mockGraphWithCycle = [
{
id: 'job_foo',
inEdges: [
{
origin: 'dataset_foo',
destination: 'job_foo'
}
],
outEdges: [
{
origin: 'job_foo',
destination: 'dataset_bar'
}
]
},
{
id: 'dataset_bar',
inEdges: [
{
origin: 'job_foo',
destination: 'dataset_bar'
}
],
outEdges: [
{
origin: 'dataset_bar',
destination: 'job_bar'
}
]
},
{
id: 'job_bar',
inEdges: [
{
origin: 'dataset_bar',
destination: 'job_bar'
}
],
outEdges: [
{
origin: 'job_bar',
destination: 'dataset_foo'
}
]
},
{
id: 'dataset_foo',
inEdges: [
{
origin: 'job_bar',
destination: 'dataset_foo'
}
],
outEdges: [
{
origin: 'dataset_foo',
destination: 'job_foo'
}
]
class MockEdge {
origin: string
destination: string

constructor(origin, destination) {
this.origin = origin
this.destination = destination
}
]
}

class MockNode implements Partial<LineageNode> {
id: string
inEdges: MockEdge[]
outEdges: MockEdge[]

constructor(id, prev: string[], next: string[]) {
this.id = id
this.inEdges = prev ? prev.map(p => new MockEdge(p, id)) : ([] as MockEdge[])
this.outEdges = next ? next.map(n => new MockEdge(id, n)) : ([] as MockEdge[])
}
}

const mockGraphWithCycle = [
new MockNode('1', ['3'], ['2']),
new MockNode('2', ['1'], ['3']),
new MockNode('3', ['2'], ['1'])
] as LineageNode[]

const mockGraphWithoutCycle = [
new MockNode('1', [], ['2', '4']),
new MockNode('2', ['1'], ['3']),
new MockNode('3', ['2'], []),
new MockNode('4', ['1'], [])
] as LineageNode[]

describe('Lineage Component', () => {
const selectedNode = 'job_foo'
let g: graphlib.Graph<MqNode>
const selectedNode = '1'
let graphWithCycle: graphlib.Graph<MqNode>

beforeEach(() => {
g = initGraph()
buildGraphAll(g, mockGraphWithCycle, (gResult: graphlib.Graph<MqNode>) => {
g = gResult
})
graphWithCycle = initGraph()
buildGraphAll(
graphWithCycle,
mockGraphWithCycle,
true,
selectedNode,
(gResult: graphlib.Graph<MqNode>) => {
graphWithCycle = gResult
}
)
})

it("doesn't follow cycles in the lineage graph", () => {
const paths = getSelectedPaths(g, selectedNode)
const paths = getSelectedPaths(graphWithCycle, selectedNode)

const pathCounts = paths.reduce((acc, p) => {
const pathId = p.join(':')
Expand All @@ -103,19 +70,32 @@ describe('Lineage Component', () => {
})

it('renders a valid cycle', () => {
const actualPaths = getSelectedPaths(g, selectedNode)
const actualPaths = getSelectedPaths(graphWithCycle, selectedNode)

const expectedPaths = [
['job_foo', 'dataset_bar'],
['dataset_bar', 'job_bar'],
['job_bar', 'dataset_foo'],
['dataset_foo', 'job_foo'],
['dataset_foo', 'job_foo'],
['job_bar', 'dataset_foo'],
['dataset_bar', 'job_bar'],
['job_foo', 'dataset_bar']
['1', '2'],
['2', '3'],
['3', '1'],
['3', '1'],
['2', '3'],
['1', '2']
]

expect(actualPaths).toEqual(expectedPaths)
})

it('includes nodes in selected path when fullGraph is true', () => {
const g = initGraph()
buildGraphAll(g, mockGraphWithoutCycle, true, '3', () => null)

expect(g.node('4')).toBeDefined()
})

it('exclude nodes not in selected path when fullGraph is false', () => {
const g = initGraph()

buildGraphAll(g, mockGraphWithoutCycle, false, '3', () => null)

expect(g.node('4')).toBeUndefined()
})
})
59 changes: 59 additions & 0 deletions web/src/components/full-graph-switch/FullGraphSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2018-2023 contributors to the Marquez project
// SPDX-License-Identifier: Apache-2.0

import * as Redux from 'redux'
import { Box, FormControlLabel, Switch } from '@mui/material'
import { IState } from '../../store/reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { setShowFullGraph } from '../../store/actionCreators'
import React from 'react'

interface FullGraphSwitch {
showFullGraph: boolean
setShowFullGraph: (showFullGraph: boolean) => void
}

const FullGraphSwitch: React.FC<FullGraphSwitch> = ({ setShowFullGraph, showFullGraph }) => {
const i18next = require('i18next')
const FULL_GRAPH_LABEL = i18next.t('lineage.full_graph_label')
return (
<Box
sx={theme => ({
display: 'flex',
justifyContent: 'space-evenly',
alignItems: 'center',
zIndex: theme.zIndex.appBar
})}
>
<FormControlLabel
sx={{
marginLeft: 0
}}
labelPlacement='start'
control={
<Switch
checked={showFullGraph}
onChange={(_, checked) => setShowFullGraph(checked)}
color='primary'
/>
}
label={FULL_GRAPH_LABEL}
/>
</Box>
)
}

const mapStateToProps = (state: IState) => ({
showFullGraph: state.lineage.showFullGraph
})

const mapDispatchToProps = (dispatch: Redux.Dispatch) =>
bindActionCreators(
{
setShowFullGraph: setShowFullGraph
},
dispatch
)

export default connect(mapStateToProps, mapDispatchToProps)(FullGraphSwitch)
Loading