From 8330394fcd42e5ac40b3abcc7bd25b87ab8acb9e Mon Sep 17 00:00:00 2001 From: Rafael Weinstein Date: Mon, 2 Nov 2015 14:23:42 -0800 Subject: [PATCH] splore now uses js2 --- clients/splore/.babelrc | 11 +++ clients/splore/.eslintrc | 35 +++++++++ clients/splore/.flowconfig | 10 +++ clients/splore/buchheim.js | 147 ++++++++++++++++++++++-------------- clients/splore/index.html | 2 +- clients/splore/layout.js | 75 ++++++++++-------- clients/splore/link.sh | 2 +- clients/splore/main.js | 118 +++++++++++++++++------------ clients/splore/node.js | 81 ++++++++++++-------- clients/splore/package.json | 13 ++-- 10 files changed, 314 insertions(+), 180 deletions(-) create mode 100644 clients/splore/.babelrc create mode 100644 clients/splore/.eslintrc create mode 100644 clients/splore/.flowconfig diff --git a/clients/splore/.babelrc b/clients/splore/.babelrc new file mode 100644 index 0000000000..5d19ab5269 --- /dev/null +++ b/clients/splore/.babelrc @@ -0,0 +1,11 @@ +{ + "optional": ["asyncToGenerator"], + "blacklist": [ + "es6.blockScoping", + "es6.constants", + "es6.forOf", + "es6.properties.computed", + "es6.properties.shorthand", + "es6.templateLiterals" + ] +} diff --git a/clients/splore/.eslintrc b/clients/splore/.eslintrc new file mode 100644 index 0000000000..1c3e995d8c --- /dev/null +++ b/clients/splore/.eslintrc @@ -0,0 +1,35 @@ +{ + "parser": "babel-eslint", + "rules": { + "array-bracket-spacing": [2, "never"], + "camelcase": 2, + "eqeqeq": 2, + "indent": [2, 2, {"SwitchCase": 1}], + "linebreak-style": [2, "unix"], + "no-multi-spaces": 2, + "no-new-wrappers": 2, + "no-throw-literal": 2, + "no-var": 2, + "object-curly-spacing": [2, "never"], + "quotes": [2, "single"], + "radix": 2, + "semi": 2, + "space-after-keywords": 2, + "space-before-function-paren": [2, "never"], + "space-infix-ops": 2, + "space-in-parens": [2, "never"] + }, + "env": { + "es6": true, + "node": true, + "browser": true + }, + "extends": "eslint:recommended", + "ecmaFeatures": { + "jsx": true, + "experimentalObjectRestSpread": true + }, + "plugins": [ + "react" + ] +} diff --git a/clients/splore/.flowconfig b/clients/splore/.flowconfig new file mode 100644 index 0000000000..0e33a20b04 --- /dev/null +++ b/clients/splore/.flowconfig @@ -0,0 +1,10 @@ +[ignore] +.*/node_modules/react/.* + +[include] + +[libs] + +[options] +unsafe.enable_getters_and_setters=true +munge_underscores=true diff --git a/clients/splore/buchheim.js b/clients/splore/buchheim.js index dc1d04ef13..34d6fcdaca 100644 --- a/clients/splore/buchheim.js +++ b/clients/splore/buchheim.js @@ -1,3 +1,5 @@ +/* @flow */ + 'use strict'; // JavaScript implementation of Christoph Buchheim, Michael Jünger, Sebastian Leipert's tree layout algorithm. See: http://dl.acm.org/citation.cfm?id=729576. @@ -5,16 +7,51 @@ // Thanks also to Bill Mill for the explanation and Python sample code: http://billmill.org/pymag-trees/. // TreeNode represents one node of the tree visualization. -class TreeNode { - constructor(data, id, parent, depth, number, seen) { + +function assertNotNull(v: ?T): T { + if (v !== null && v !== undefined) { + return v; + } + + throw new Error('Non-null assertion failed'); +} + +export type NodeData = { + name: string, + fullName: string, + isOpen: boolean, + canOpen: boolean +}; + +export type NodeGraph = { + nodes: {[key: string]: NodeData}, + links: {[key: string]: Array} +}; + +export class TreeNode { + x: number; + y: number; + data: NodeData; + id: string; + children: Array; + parent: ?TreeNode; + thread: ?TreeNode; + offset: number; + ancestor: TreeNode; + change: number; + shift: number; + number: number; + mod: number; + + constructor(graph: NodeGraph, id: string, parent: ?TreeNode, depth: number, number: number, seen: {[key: string]: boolean}) { seen[id] = true; this.x = -1; this.y = depth; - this.data = data.nodes[id]; + this.data = graph.nodes[id]; this.id = id; - this.children = ((this.data.isOpen && data.links[id]) || []) + this.children = ((this.data.isOpen && graph.links[id]) || []) .filter(cid => !(cid in seen)) - .map((cid, i) => new TreeNode(data, cid, this, depth+1, i+1, seen)); + .map((cid, i) => new TreeNode(graph, cid, this, depth + 1, i + 1, seen)); this.parent = parent; this.thread = null; this.offset = 0; @@ -25,24 +62,24 @@ class TreeNode { this.mod = 0; } - left() { + left(): ?TreeNode { if (this.children.length > 0) { return this.children[0]; } return this.thread; } - right() { + right(): ?TreeNode { if (this.children.length > 0) { return this.children[this.children.length - 1]; } return this.thread; } - leftBrother() { - var n = null; + leftBrother(): ?TreeNode { + let n = null; if (this.parent) { - for (var node of this.parent.children) { + for (let node of this.parent.children) { if (node === this) { return n; } else { @@ -53,8 +90,8 @@ class TreeNode { return n; } - getLeftMostSibling() { - if (this.parent && this != this.parent.children[0]) { + getLeftMostSibling(): ?TreeNode { + if (this.parent && this !== this.parent.children[0]) { return this.parent.children[0]; } else { return null; @@ -62,31 +99,29 @@ class TreeNode { } } -function layout(tree) { +export function layout(tree: TreeNode): void{ firstWalk(tree, 1); secondWalk(tree, 0, 0); } -function firstWalk(v, distance) { +function firstWalk(v: TreeNode, distance: number): void { if (v.children.length === 0) { if (v.getLeftMostSibling()) { - v.x = v.leftBrother().x + distance; + v.x = assertNotNull(v.leftBrother()).x + distance; } else { v.x = 0; } } else { - var defaultAncestor = v.children[0]; - for (var w of v.children) { + let defaultAncestor = v.children[0]; + for (let w of v.children) { firstWalk(w, distance); defaultAncestor = apportion(w, defaultAncestor, distance); } executeShifts(v); - var midpoint = (v.children[0].x + v.children[v.children.length - 1].x) / 2; + let midpoint = (v.children[0].x + v.children[v.children.length - 1].x) / 2; - var ell = v.children[0]; - var arr = v.children[v.children.length - 1]; - var w = v.leftBrother(); + let w = v.leftBrother(); if (w) { v.x = w.x + distance; v.mod = v.x - midpoint; @@ -94,30 +129,28 @@ function firstWalk(v, distance) { v.x = midpoint; } } - - return v; } -function apportion(v, defaultAncestor, distance) { - var w = v.leftBrother(); - if (w != null) { - var vir = v; - var vor = v; - var vil = w; - var vol = v.getLeftMostSibling(); - var sir = v.mod; - var sor = v.mod; - var sil = vil.mod; - var sol = vol.mod; +function apportion(v: TreeNode, defaultAncestor: TreeNode, distance: number): TreeNode { + let w = v.leftBrother(); + if (w !== null) { + let vir = v; + let vor = v; + let vil = assertNotNull(w); + let vol = assertNotNull(v.getLeftMostSibling()); + let sir = v.mod; + let sor = v.mod; + let sil = vil.mod; + let sol = vol.mod; while (vil.right() && vir.left()) { - vil = vil.right(); - vir = vir.left(); - vol = vol.left(); - vor = vor.right(); - vor.ancestor = v - var shift = (vil.x + sil) - (vir.x + sir) + distance; + vil = assertNotNull(vil.right()); + vir = assertNotNull(vir.left()); + vol = assertNotNull(vol.left()); + vor = assertNotNull(vor.right()); + vor.ancestor = v; + let shift = (vil.x + sil) - (vir.x + sir) + distance; if (shift > 0) { - var a = ancestor(vil, v, defaultAncestor); + let a = ancestor(vil, v, defaultAncestor); moveSubtree(a, v, shift); sir = sir + shift; sor = sor + shift; @@ -138,11 +171,11 @@ function apportion(v, defaultAncestor, distance) { defaultAncestor = v; } } - return defaultAncestor + return defaultAncestor; } -function moveSubtree(wl, wr, shift) { - var subtrees = wr.number - wl.number; +function moveSubtree(wl: TreeNode, wr: TreeNode, shift: number): void { + let subtrees = wr.number - wl.number; wr.change -= shift / subtrees; wr.shift += shift; wl.change += shift / subtrees; @@ -150,11 +183,11 @@ function moveSubtree(wl, wr, shift) { wr.mod += shift; } -function executeShifts(v) { - var shift = 0; - var change = 0; - for (var i = v.children.length - 1; i >= 0; i--) { - var w = v.children[i]; +function executeShifts(v: TreeNode): void { + let shift = 0; + let change = 0; + for (let i = v.children.length - 1; i >= 0; i--) { + let w = v.children[i]; w.x += shift; w.mod += shift; change += w.change; @@ -162,21 +195,19 @@ function executeShifts(v) { } } -function ancestor(vil, v, defaultAncestor) { - if (v.parent.children.indexOf(vil.ancestor) > -1) { +function ancestor(vil: TreeNode, v: TreeNode, defaultAncestor: TreeNode): TreeNode { + if (v.parent && v.parent.children.indexOf(vil.ancestor) > -1) { return vil.ancestor; } else { return defaultAncestor; } } -function secondWalk(v, m, depth) { - v.x += m - v.y = depth +function secondWalk(v: TreeNode, m: number, depth: number): void { + v.x += m; + v.y = depth; - for (var w of v.children) { - secondWalk(w, m + v.mod, depth+1) + for (let w of v.children) { + secondWalk(w, m + v.mod, depth + 1); } } - -module.exports = {TreeNode, layout}; diff --git a/clients/splore/index.html b/clients/splore/index.html index acf22d0d71..38ad37a15f 100644 --- a/clients/splore/index.html +++ b/clients/splore/index.html @@ -5,4 +5,4 @@ - +
diff --git a/clients/splore/layout.js b/clients/splore/layout.js index 5e83ed3ff4..9ceba203ab 100644 --- a/clients/splore/layout.js +++ b/clients/splore/layout.js @@ -1,44 +1,55 @@ +/* @flow */ + 'use strict'; -var Node = require('./node.js'); -var React = require('react'); +import Node from './node.js'; +import React from 'react'; +import type {NodeGraph} from './buchheim.js'; +import {TreeNode} from './buchheim.js'; -var Layout = React.createClass({ - propTypes: { - data: React.PropTypes.object.isRequired, - onNodeClick: React.PropTypes.func.isRequired, - tree: React.PropTypes.object.isRequired, - }, +type Props = { + data: NodeGraph, + onNodeClick: (e: Event, s: string) => void, + tree: TreeNode +} - render() { - var children = []; - var edges = []; - var lookup = {}; +export default class Layout extends React.Component { + static defaultProps: {}; + props: Props; + + constructor(props: Props) { + super(props); + } + + render(): React.Element { + let children = []; + let edges = []; + let lookup = {}; const spaceX = 75; const spaceY = 20; - var getX = d => d.y * spaceX; - var getY = d => d.x * spaceY; - var maxX = 0; - var minY = 0; - var maxY = 0; + let getX = d => d.y * spaceX; + let getY = d => d.x * spaceY; + let maxX = 0; + let minY = 0; + let maxY = 0; - var process = (treeNode, fromX, fromY) => { - var links = this.props.data.links[treeNode.id] || []; - var hasChildren = treeNode.data.canOpen || links.length > 0; - var x = getX(treeNode); - var y = getY(treeNode); - var title = ''; + let process = (treeNode, fromX, fromY) => { + let links = this.props.data.links[treeNode.id] || []; + let hasChildren = treeNode.data.canOpen || links.length > 0; + let x = getX(treeNode); + let y = getY(treeNode); + let title = ''; if (treeNode.data.fullName) { - title = 'Alt-click for full ref' + title = 'Alt-click for full ref'; } maxX = Math.max(x + spaceX, maxX); minY = Math.min(y, minY); maxY = Math.max(y + spaceY, maxY); - var n = ( + let n = ( { - var from = lookup[e[0]]; - var to = lookup[e[1]]; + let from = lookup[e[0]]; + let to = lookup[e[1]]; children.push( ); }); - var sortOrder = (elm => elm.type == 'path' ? 0 : 1); + let sortOrder = (elm => elm.type === 'path' ? 0 : 1); children.sort((a, b) => sortOrder(a) - sortOrder(b)); - var translateY = spaceY; + let translateY = spaceY; if (minY < 0) { translateY -= minY; maxY -= minY; @@ -89,7 +100,5 @@ var Layout = React.createClass({ ); - }, -}); - -module.exports = React.createFactory(Layout); + } +} diff --git a/clients/splore/link.sh b/clients/splore/link.sh index 53ea642855..d64611d4c7 100755 --- a/clients/splore/link.sh +++ b/clients/splore/link.sh @@ -1,4 +1,4 @@ -pushd ../../js +pushd ../../js2 npm run build ./link.sh popd diff --git a/clients/splore/main.js b/clients/splore/main.js index b5e6ec3249..f8cd24d7eb 100644 --- a/clients/splore/main.js +++ b/clients/splore/main.js @@ -1,41 +1,43 @@ +/* @flow */ + 'use strict'; -var buchheim = require('./buchheim.js'); -var Layout = require('./layout.js'); -var Immutable = require('immutable'); -var noms = require('noms'); -var queryString = require('query-string'); -var React = require('react'); -var {Ref} = require('noms'); +import {layout, NodeGraph, TreeNode} from './buchheim.js'; +import Layout from './layout.js'; +import React from 'react'; //eslint-disable-line no-unused-vars +import ReactDOM from 'react-dom'; +import {readValue, HttpStore, Ref} from 'noms'; -var data = {nodes: {}, links: {}}; -var rootRef = null; +let data: NodeGraph = {nodes: {}, links: {}}; +let rootRef: Ref; +let httpStore: HttpStore; +let renderNode: ?HTMLElement; -window.onload = function() { - var target = document.getElementById('root'); - var w = window.innerWidth; - var h = window.innerHeight; +window.addEventListener('load', () => { + renderNode = document.getElementById('splore'); + httpStore = new HttpStore('http://localhost:8000'); - noms.getRoot().then(ref => { + httpStore.getRoot().then(ref => { rootRef = ref; - handleChunkLoad(ref, new Ref(ref)); + handleChunkLoad(ref, ref); }); -}; +}); window.onresize = render; -function handleChunkLoad(ref, val, fromRef) { - var counter = 0 - var process = (ref, val, fromId) => { - if (typeof val == 'undefined') { +function handleChunkLoad(ref: Ref, val: any, fromRef: ?string) { + let counter = 0; + + function process(ref, val, fromId): ?string { + if (typeof val === 'undefined') { return null; } // Assign a unique ID to this node. // We don't use the noms ref because we only want to represent values as shared in the graph if they are actually in the same chunk. - var id; + let id; if (val instanceof Ref) { - id = val.ref; + id = val.toString(); } else { id = ref + '/' + counter++; } @@ -55,48 +57,65 @@ function handleChunkLoad(ref, val, fromRef) { if (val instanceof Blob) { data.nodes[id] = {name: `Blob (${val.size})`}; - } else if (Immutable.List.isList(val)) { - data.nodes[id] = {name: `List (${val.size})`}; + } else if (Array.isArray(val)) { + data.nodes[id] = {name: `List (${val.length})`}; val.forEach(c => process(ref, c, id)); - } else if (Immutable.Set.isSet(val)) { + } else if (val instanceof Set) { data.nodes[id] = {name: `Set (${val.size})`}; val.forEach(c => process(ref, c, id)); - } else if (Immutable.Map.isMap(val)) { - var structName = val.get('$name'); - if (structName) { - data.nodes[id] = {name: structName}; - } else { - data.nodes[id] = {name: `Map (${val.size})`}; - } - val.keySeq().filter(k => k != '$name') - .forEach(k => { - // TODO: handle non-string keys - var kid = process(ref, k, id); - + } else if (val instanceof Map) { + data.nodes[id] = {name: `Map (${val.size})`}; + val.forEach((v, k) => { + // TODO: handle non-string keys + let kid = process(ref, k, id); + if (kid) { // Start map keys open, just makes it easier to use. data.nodes[kid].isOpen = true; - process(ref, val.get(k), kid); - }); + process(ref, v, kid); + } else { + throw new Error('No kid id.'); + } + }); } else if (val instanceof Ref) { + let refStr = val.toString(); data.nodes[id] = { canOpen: true, - name: val.ref.substr(5, 6), - fullName: val.ref, + name: refStr.substr(5, 6), + fullName: refStr }; + } else if (val._typeRef) { + // Struct + let structName = val._typeRef.name; + data.nodes[id] = {name: structName}; + Object.keys(val).forEach(k => { + if (k === '_typeRef') { + return; + } + // TODO: handle non-string keys + let kid = process(ref, k, id); + if (kid) { + // Start map keys open, just makes it easier to use. + data.nodes[kid].isOpen = true; + + process(ref, val[k], kid); + } else { + throw new Error('No kid id.'); + } + }); } return id; - }; + } process(ref, val, fromRef); render(); } -function handleNodeClick(e, id) { +function handleNodeClick(e: Event, id: string) { if (e.altKey) { if (data.nodes[id].fullName) { - window.prompt("Full ref", data.nodes[id].fullName); + window.prompt('Full ref', data.nodes[id].fullName); } return; } @@ -111,14 +130,15 @@ function handleNodeClick(e, id) { if (data.links[id] || !data.nodes[id].isOpen) { render(); } else { - noms.readValue(id, noms.getChunk) - .then(chunk => handleChunkLoad(id, chunk, id)); + readValue(Ref.parse(id), httpStore).then(value => { + handleChunkLoad(id, value, id); + }); } } } function render() { - var dt = new buchheim.TreeNode(data, rootRef, null, 0, 0, {}); - buchheim.layout(dt); - React.render(, document.body); + let dt = new TreeNode(data, rootRef.toString(), null, 0, 0, {}); + layout(dt); + ReactDOM.render(, renderNode); } diff --git a/clients/splore/node.js b/clients/splore/node.js index b8c25c46c1..6e99d7834c 100644 --- a/clients/splore/node.js +++ b/clients/splore/node.js @@ -1,41 +1,58 @@ +/* @flow */ + 'use strict'; -var classNames = require('classnames'); -var React = require('react'); +import classNames from 'classnames'; +import React from 'react'; -var Node = React.createClass({ - propTypes: { - canOpen: React.PropTypes.bool.isRequired, - isOpen: React.PropTypes.bool.isRequired, - shape: React.PropTypes.string.isRequired, - text: React.PropTypes.string.isRequired, - title: React.PropTypes.string, - fromX: React.PropTypes.number.isRequired, - fromY: React.PropTypes.number.isRequired, - x: React.PropTypes.number.isRequired, - y: React.PropTypes.number.isRequired, - onClick: React.PropTypes.func.isRequired, - }, +type Props = { + canOpen: boolean, + isOpen: boolean, + shape: string, + text: string, + title: string, + fromX: number, + fromY: number, + x: number, + y: number, + onClick: (e: Event, s: String) => void; +} - getInitialState() { - return { +type State = { + x: number, + y: number +} + +export default class Node extends React.Component { + static defaultProps: {}; + props: Props; + state: State; + + constructor(props: Props) { + super(props); + + this.state = { x: this.props.fromX, - y: this.props.fromY, + y: this.props.fromY }; - }, + } - render() { - if (this.state.x != this.props.x || - this.state.y != this.props.y) { + setState(state: State) { + super.setState(state); + } + + render(): React.Element { + if (this.state.x !== this.props.x || + this.state.y !== this.props.y) { window.requestAnimationFrame(() => this.setState({ x: this.props.x, - y: this.props.y, + y: this.props.y })); } - var textAnchor = 'start'; - var textX = 10; - var translate = `translate3d(${this.state.x}px, ${this.state.y}px, 0)`; + let textAnchor = 'start'; + let textX = 10; + let translate = `translate3d(${this.state.x}px, ${this.state.y}px, 0)`; if (this.props.canOpen) { textAnchor = 'end'; @@ -51,10 +68,10 @@ var Node = React.createClass({ {this.props.title} ); - }, + } getShape() { - var className = classNames('icon', {open:this.props.isOpen}); + let className = classNames('icon', {open:this.props.isOpen}); switch (this.props.shape) { case 'circle': return ; @@ -62,9 +79,7 @@ var Node = React.createClass({ // rx:1.35 and ry:1.35 for rounded corners, but not doing until I learn how to make the triangle match below. return ; case 'triangle': - return + return ; } - }, -}); - -module.exports = React.createFactory(Node); + } +} diff --git a/clients/splore/package.json b/clients/splore/package.json index 770a502a12..d211491af9 100644 --- a/clients/splore/package.json +++ b/clients/splore/package.json @@ -2,18 +2,21 @@ "name": "noms-splore", "dependencies": { "classnames": "^2.1.3", - "immutable": "^3.7.4", - "query-string": "^2.4.0", - "react": "^0.12.0" + "react": "^0.14.1", + "react-dom": "^0.14.1" }, "devDependencies": { + "babel": "^5.6.23", + "babel-eslint": "^4.1.3", "babelify": "^6.1.3", "browserify": "^6.2.0", - "uglify-js": "~2.4.15", + "eslint": "^1.7.3", + "eslint-plugin-react": "^3.6.3", "watchify": "^2.1.1" }, "scripts": { "start": "watchify -o out.js -t babelify -v -d main.js", - "build": "NODE_ENV=production browserify main.js -t babelify | uglifyjs -cm > out.js" + "build": "NODE_ENV=production browserify main.js -t babelify > out.js", + "test": "rm -f out.js && eslint *.js && flow" } }