Skip to content

Commit f2111ef

Browse files
author
Alan Shaw
committed
Merge branch 'master' of github.com:ipfs-shipyard/ipfs-dag-builder-vis
2 parents f98f351 + a59007b commit f2111ef

File tree

12 files changed

+421
-221
lines changed

12 files changed

+421
-221
lines changed

package-lock.json

Lines changed: 58 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
"ipfs": "^0.35.0",
1010
"ipfs-css": "^0.12.0",
1111
"parcel": "^1.12.3",
12+
"react": "^16.8.6",
13+
"react-dom": "^16.8.6",
1214
"tachyons": "^4.11.1"
1315
},
1416
"devDependencies": {
17+
"babel-eslint": "^10.0.1",
1518
"standard": "^12.0.1"
1619
},
1720
"scripts": {
@@ -33,5 +36,8 @@
3336
"license": "MIT",
3437
"browserslist": [
3538
"last 2 Chrome versions"
36-
]
39+
],
40+
"standard": {
41+
"parser": "babel-eslint"
42+
}
3743
}

src/App.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* eslint-env browser */
2+
import React, { useState, useEffect } from 'react'
3+
import Header from './Header'
4+
import Controls from './Controls'
5+
import Dag from './Dag'
6+
import { Buffer } from 'ipfs'
7+
import { ipfsAdd } from './lib/ipfs'
8+
import DropTarget from './DropTarget'
9+
10+
export default function App () {
11+
const [files, setFiles] = useState([])
12+
const [chunker, setChunker] = useState('size-512')
13+
const [strategy, setStrategy] = useState('balanced')
14+
const [maxChildren, setMaxChildren] = useState(11)
15+
const [layerRepeat, setLayerRepeat] = useState(4)
16+
const [rootCid, setRootCid] = useState(null)
17+
18+
useEffect(() => {
19+
if (!files.length) return
20+
ipfsAdd({ files, chunker, strategy, maxChildren, layerRepeat }).then(setRootCid)
21+
}, [files, chunker, strategy, maxChildren, layerRepeat])
22+
23+
const onFileChange = file => {
24+
const fileReader = new FileReader()
25+
fileReader.onload = e => {
26+
setFiles(files.concat({ path: file.name, content: Buffer.from(e.target.result) }))
27+
}
28+
fileReader.readAsArrayBuffer(file)
29+
}
30+
31+
const onReset = () => {
32+
setFiles([])
33+
setChunker('size-512')
34+
setStrategy('balanced')
35+
setMaxChildren(11)
36+
setLayerRepeat(4)
37+
setRootCid(null)
38+
}
39+
40+
return (
41+
<div className='avenir flex flex-column h-100'>
42+
<div className='flex-none'>
43+
<Header />
44+
</div>
45+
<div className='flex-none'>
46+
<Controls
47+
chunker={chunker}
48+
onChunkerChange={setChunker}
49+
strategy={strategy}
50+
onStrategyChange={setStrategy}
51+
maxChildren={maxChildren}
52+
onMaxChildrenChange={setMaxChildren}
53+
layerRepeat={layerRepeat}
54+
onLayerRepeatChange={setLayerRepeat}
55+
onReset={onReset} />
56+
</div>
57+
<div className='flex-auto'>
58+
<DropTarget onFileDrop={onFileChange} className='h-100'>
59+
{files.length ? <Dag rootCid={rootCid} /> : null}
60+
</DropTarget>
61+
</div>
62+
</div>
63+
)
64+
}

src/Controls.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from 'react'
2+
3+
export default function Controls ({
4+
chunker,
5+
onChunkerChange,
6+
strategy,
7+
onStrategyChange,
8+
maxChildren,
9+
onMaxChildrenChange,
10+
layerRepeat,
11+
onLayerRepeatChange,
12+
onReset
13+
}) {
14+
return (
15+
<div className='flex flex-row items-center pa3 bg-white'>
16+
<div className='mr3'>
17+
<select
18+
value={chunker}
19+
onChange={e => onChunkerChange(e.target.value)}
20+
className='charcoal ba b--black-20 br1 pv1 ph2 db center focus-outline'>
21+
<option value='size-32'>32 byte chunks</option>
22+
<option value='size-512'>512 byte chunks</option>
23+
<option value='size-1024'>1,024 byte chunks</option>
24+
<option value='size-16384'>16,384 byte chunks</option>
25+
<option value='size-262144'>26,2144 byte chunks</option>
26+
</select>
27+
</div>
28+
<div className='mr3'>
29+
<select
30+
value={strategy}
31+
onChange={e => onStrategyChange(e.target.value)}
32+
className='charcoal ba b--black-20 br1 pv1 ph2 db center focus-outline'>
33+
<option value='balanced'>Balanced DAG</option>
34+
<option value='trickle'>Trickle DAG</option>
35+
<option value='flat'>Flat DAG</option>
36+
</select>
37+
</div>
38+
{['balanced', 'trickle'].includes(strategy) ? (
39+
<div className='mr3'>
40+
<select
41+
value={maxChildren}
42+
onChange={e => onMaxChildrenChange(parseInt(e.target.value))}
43+
className='charcoal ba b--black-20 br1 pv1 ph2 db center focus-outline'>
44+
<option value='11'>11 children max</option>
45+
<option value='44'>44 children max</option>
46+
<option value='174'>174 children max</option>
47+
</select>
48+
</div>
49+
) : null}
50+
{strategy === 'trickle' ? (
51+
<div className='mr3'>
52+
<select
53+
value={layerRepeat}
54+
onChange={e => onLayerRepeatChange(parseInt(e.target.value))}
55+
className='charcoal ba b--black-20 br1 pv1 ph2 db center focus-outline'>
56+
<option value='1'>1 layer repeat</option>
57+
<option value='4'>4 layer repeats</option>
58+
<option value='16'>16 layer repeats</option>
59+
</select>
60+
</div>
61+
) : null}
62+
<div className='flex-auto' />
63+
<button
64+
type='button'
65+
onClick={e => onReset()}
66+
className='transition-all sans-serif dib v-mid fw5 nowrap lh-copy bn br1 ph4 pv1 pointer focus-outline bg-gray hover-bg-red white'
67+
title='Clear file(s) and reset controls to defaults'>
68+
Reset
69+
</button>
70+
</div>
71+
)
72+
}

src/Dag.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React, { Component, createRef } from 'react'
2+
import cytoscape from 'cytoscape'
3+
import dagre from 'cytoscape-dagre'
4+
import UnixFs from 'ipfs-unixfs'
5+
import { getIpfs } from './lib/ipfs'
6+
import DagGraphOptions from './DagGraphOptions'
7+
8+
cytoscape.use(dagre)
9+
10+
export default class Dag extends Component {
11+
constructor () {
12+
super()
13+
this._graphRoot = createRef()
14+
}
15+
16+
componentDidMount () {
17+
this._updateGraph()
18+
}
19+
20+
componentDidUpdate () {
21+
this._updateGraph()
22+
}
23+
24+
async _updateGraph () {
25+
if (this._graph) this._graph.destroy()
26+
27+
const { rootCid } = this.props
28+
if (!rootCid) return
29+
30+
const nodeMap = await this._getGraphNodes(rootCid)
31+
32+
// ...could have taken a while, did we get a new root node?
33+
if (rootCid !== this.props.rootCid) return
34+
35+
const container = this._graphRoot.current
36+
const elements = Array.from(nodeMap.values())
37+
38+
this._graph = cytoscape({ elements, container, ...DagGraphOptions })
39+
this._graph.layout(DagGraphOptions.layout).run()
40+
}
41+
42+
async _getGraphNodes (cid, nodeMap = new Map()) {
43+
if (nodeMap.get(cid)) return
44+
45+
const ipfs = await getIpfs()
46+
const { value: source } = await ipfs.dag.get(cid)
47+
48+
let nodeData = {}
49+
50+
try {
51+
// it's a unix system?
52+
nodeData = UnixFs.unmarshal(source.data)
53+
} catch (err) {
54+
// dag-pb but not a unixfs.
55+
console.log(err)
56+
}
57+
58+
for (let i = 0; i < source.links.length; i++) {
59+
await this._getGraphNodes(source.links[i].cid.toString(), nodeMap)
60+
}
61+
62+
nodeMap.set(cid, {
63+
group: 'nodes',
64+
data: { id: cid, ...nodeData },
65+
classes: source.links.length ? [] : ['leaf']
66+
})
67+
68+
source.links.forEach(link => {
69+
nodeMap.set(cid + '->' + link.cid, {
70+
group: 'edges',
71+
data: { source: cid, target: link.cid.toString() }
72+
})
73+
})
74+
75+
return nodeMap
76+
}
77+
78+
render () {
79+
return <div ref={this._graphRoot} className='bg-snow-muted h-100' />
80+
}
81+
}

src/DagGraphOptions.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export default {
2+
layout: {
3+
name: 'dagre',
4+
rankSep: 80,
5+
nodeSep: 1
6+
},
7+
style: [
8+
{
9+
selector: 'node',
10+
style: {
11+
shape: 'ellipse',
12+
width: '14px',
13+
height: '14px',
14+
'background-color': '#244e66'
15+
}
16+
},
17+
{
18+
selector: '.leaf',
19+
style: {
20+
shape: 'square',
21+
width: '14px',
22+
height: '14px',
23+
'background-color': '#28CA9F'
24+
}
25+
},
26+
{
27+
selector: 'edge',
28+
style: {
29+
'source-distance-from-node': 3,
30+
'target-distance-from-node': 4,
31+
'curve-style': 'bezier',
32+
'control-point-weight': 0.5,
33+
'width': 1,
34+
'line-color': '#979797',
35+
'line-style': 'dotted',
36+
// 'target-label': 'data(index)',
37+
'font-family': 'Consolas, monaco, monospace',
38+
'font-size': '8px',
39+
'target-text-margin-x': '-5px',
40+
'color': '#ccc',
41+
'target-text-margin-y': '-2px',
42+
'text-halign': 'center',
43+
'text-valign': 'bottom'
44+
}
45+
}
46+
]
47+
}

0 commit comments

Comments
 (0)