src/RelExprTree.js
// @flow
import React from 'react';
import TreeMenu, {ItemComponent} from 'react-simple-tree-menu';
import {v4 as uuidv4} from 'uuid';
import {
Projection,
Rename,
Selection,
Except,
Product,
Join,
Intersect,
Union,
GroupBy,
} from './RelOp';
import {exprToString} from './util';
import {changeExpr} from './modules/data';
import {useReactGA} from './contexts/ReactGAContext';
import '../node_modules/react-simple-tree-menu/dist/main.css';
import './RelExprTree.css';
import type {Node, StatelessFunctionalComponent} from 'react';
type Props = {
changeExpr: typeof changeExpr,
expr: {[string]: any},
ReactGA?: any, // For backwards compatibility with tests
};
/** A graphical representation of a relational algebra expression */
const RelExprTree: StatelessFunctionalComponent<Props> = (props) => {
const contextReactGA = useReactGA();
const ReactGA = props.ReactGA || contextReactGA;
/**
* @param expr - a relational algebra expression object to render
* @param keys - an array where all created paths should be saved
* @param path - the path to the current node
* @return a tree structure representing the exppression
*/
const buildTree = (
expr: {[string]: any},
keys: Array<string>,
path: Array<string> = []
): {} => {
// Don't try to render empty expressions
if (!expr || Object.keys(expr).length === 0) {
return {};
}
// Save the constructed path so we can set this open later
const key = uuidv4();
const newPath = [...path, key];
keys.push(newPath.join('/'));
const type = Object.keys(expr)[0];
switch (type) {
case 'projection':
case 'selection':
case 'rename':
case 'group_by':
return {
key: key,
expr: expr,
nodes: [buildTree(expr[type].children[0], keys, newPath)],
};
case 'relation':
return {
key: key,
expr: expr,
};
case 'except':
case 'intersect':
case 'join':
case 'product':
case 'union':
return {
key: key,
expr: expr,
nodes: [
buildTree(expr[type].left, keys, newPath),
buildTree(expr[type].right, keys, newPath),
],
};
default:
throw new Error('Invalid expression ' + JSON.stringify(expr) + '.');
}
};
const getLabel = (expr: {[string]: any}): Node => {
if (!expr || Object.keys(expr).length === 0) {
return '';
}
const type = Object.keys(expr)[0];
switch (type) {
case 'projection':
return <Projection project={expr.projection.arguments.project} />;
case 'selection':
return (
<Selection
select={exprToString(
expr.selection.arguments.select ||
expr.selection.arguments.condition
)}
/>
);
case 'rename':
return <Rename rename={expr.rename.arguments.rename} />;
case 'group_by':
return (
<GroupBy
groupBy={expr.group_by.arguments.groupBy}
aggregates={expr.group_by.arguments.aggregates.map(
(agg) => `${agg.aggregate.function}(${agg.aggregate.column})`
)}
selectColumns={expr.group_by.arguments.selectColumns}
/>
);
case 'relation':
return expr.relation;
case 'join':
return (
<Join
type={expr.join.type}
condition={exprToString(expr.join.condition)}
/>
);
case 'except':
case 'intersect':
case 'product':
case 'union':
const operator = {
except: <Except />,
intersect: <Intersect />,
product: <Product />,
union: <Union />,
}[type];
return operator;
default:
throw new Error('Invalid expression ' + JSON.stringify(expr) + '.');
}
};
const keys: Array<string> = [];
const data = [buildTree(props.expr, keys)];
return (
<div className="RelExprTree">
<TreeMenu
data={data}
initialOpenNodes={keys}
hasSearch={false}
disableKeyboard
onClickItem={(clickProps) => {
props.changeExpr(clickProps.expr, null);
if (ReactGA) {
ReactGA.event({
category: 'User Selecting Relational Algebra Tree',
action: Object.keys(clickProps.expr)[0],
});
}
}}
>
{({search, items}) => (
<div>
{items.map(({key, ...props}) => {
const newProps = {label: getLabel(props.expr)};
Object.assign(props, newProps);
return <ItemComponent key={key} {...props} />;
})}
</div>
)}
</TreeMenu>
</div>
);
};
export default RelExprTree;