Home Reference Source Test

src/RelExpr.js

// @flow
import React, {useEffect, useRef, useState} from 'react';
import {
  UnaryRelOp,
  Projection,
  Rename,
  Selection,
  BinaryRelOp,
  Except,
  OrderBy,
  Product,
  Join,
  Intersect,
  Union,
  GroupBy,
} from './RelOp';
import Relation from './Relation';
import {exprToString} from './util';
import {changeExpr} from './modules/data';
import {useReactGA} from './contexts/ReactGAContext';

import './RelExpr.css';

import type {Node, StatelessFunctionalComponent} from 'react';
import {useSelector} from 'react-redux';
import type {State} from './modules/relexp';

type Props = {
  changeExpr: typeof changeExpr,
  expr: {[string]: any},
  ReactGA?: any, // For backwards compatibility with tests
};

/** A graphical representation of a relational algebra expression */
const RelExpr: StatelessFunctionalComponent<Props> = (props) => {
  const nodeRef = useRef<?HTMLSpanElement>();
  const currentNode = useSelector<{data: State}, _>((state) => state.data.expr);
  const [isHovering, setIsHovering] = useState(false);
  const [isSelected, setIsSelected] = useState(false);
  const contextReactGA = useReactGA();
  const ReactGA = props.ReactGA || contextReactGA;

  useEffect(() => {
    // Adjust 'clicked' highlighting based on any changes to currentNode selected
    const clicked = props.expr === currentNode;
    if (!clicked) {
      setIsSelected(false);
      setIsHovering(false);
    }
  }, [currentNode, props.expr]);

  /**
   * @param expr - a relational algebra expression object to render
   * @return a component representing the top-most expression
   */
  const buildExpr = (expr: {[string]: any}): Node => {
    // Don't try to render empty expressions
    if (!expr || Object.keys(expr).length === 0) {
      return '';
    }
    const type = Object.keys(expr)[0];
    switch (type) {
      case 'projection':
        return (
          <span>
            <UnaryRelOp
              operator={
                <Projection project={expr.projection.arguments.project} />
              }
            >
              <RelExpr
                expr={expr.projection.children[0]}
                changeExpr={props.changeExpr}
              />
            </UnaryRelOp>
          </span>
        );

      case 'selection':
        return (
          <span>
            <UnaryRelOp
              operator={
                <Selection
                  select={exprToString(expr.selection.arguments.select)}
                />
              }
            >
              <RelExpr
                expr={expr.selection.children[0]}
                changeExpr={props.changeExpr}
              />
            </UnaryRelOp>
          </span>
        );

      case 'rename':
        return (
          <span>
            <UnaryRelOp
              operator={<Rename rename={expr.rename.arguments.rename} />}
            >
              <RelExpr
                expr={expr.rename.children[0]}
                changeExpr={props.changeExpr}
              />
            </UnaryRelOp>
          </span>
        );

      case 'relation':
        return <Relation name={expr.relation} />;

      case 'order_by':
        return (
          <OrderBy
            columns={expr.order_by.arguments.order_by}
            relation={buildExpr(expr.order_by.children[0])}
          />
        );

      case 'group_by':
        return (
          <span>
            <UnaryRelOp
              operator={
                <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}
                />
              }
            >
              <RelExpr
                expr={expr.group_by.children[0]}
                changeExpr={props.changeExpr}
              />
            </UnaryRelOp>
          </span>
        );

      case 'join':
        return (
          <Join
            type={expr.join.type}
            condition={exprToString(expr.join.condition)}
            left={buildExpr(expr.join.left)}
            right={buildExpr(expr.join.right)}
          />
        );

      case 'except':
      case 'intersect':
      case 'product':
      case 'union':
        const operator = {
          except: <Except />,
          intersect: <Intersect />,
          product: <Product />,
          union: <Union />,
        }[type];
        return (
          <BinaryRelOp
            operator={operator}
            left={
              <RelExpr expr={expr[type].left} changeExpr={props.changeExpr} />
            }
            right={
              <RelExpr expr={expr[type].right} changeExpr={props.changeExpr} />
            }
          />
        );

      default:
        throw new Error('Invalid expression ' + JSON.stringify(expr) + '.');
    }
  };

  /**
   * @param e - the event object which generated the click
   */
  const handleExprClick = (e: SyntheticMouseEvent<HTMLElement>): void => {
    e.stopPropagation();
    const node = nodeRef.current;

    const clicked = e.type === 'click' && !isSelected;

    if (props.changeExpr && node instanceof HTMLElement) {
      if (clicked) {
        props.changeExpr(props.expr, node);
      } else {
        props.changeExpr({}, node);
      }

      setIsHovering(false);
      setIsSelected(clicked);
    }

    if (ReactGA) {
      ReactGA.event({
        category: 'User Selecting Relational Algebra Enclosure',
        action: Object.keys(props.expr)[0],
      });
    }
  };

  const handleExprHover = (e: SyntheticMouseEvent<HTMLElement>): void => {
    e.stopPropagation();
    setIsHovering(e.type === 'mouseover');
  };

  if (!props.expr || Object.keys(props.expr).length === 0) {
    return '';
  }
  const type = Object.keys(props.expr)[0];
  let className = 'RelExpr';
  if (isHovering) {
    className += ' hovering';
  }
  if (isSelected) {
    className += ' clicked';
  }
  if (type === 'relation') {
    return (
      <span className="RelExpr" style={{margin: '.4em'}}>
        {buildExpr(props.expr)}
      </span>
    );
  } else {
    return (
      <span
        className={className}
        onMouseOver={handleExprHover}
        onMouseOut={handleExprHover}
        onClick={handleExprClick}
        style={{margin: '.4em'}}
        ref={nodeRef}
      >
        {buildExpr(props.expr)}
      </span>
    );
  }
};

export default RelExpr;