Skip to content
Snippets Groups Projects
Debugger.js 8.24 KiB
import React from 'react';

import CodeMirror from 'codemirror';

import * as spreadsheet from 'loot-core/src/client/sheetql/spreadsheet';
import {
  send,
  init as initConnection,
} from 'loot-core/src/platform/client/fetch';
import {
  View,
  Button,
  Input,
  InlineField,
} from 'loot-design/src/components/common';
import { colors } from 'loot-design/src/style';

require('codemirror/lib/codemirror.css');
require('codemirror/theme/monokai.css');

class Debugger extends React.Component {
  state = {
    recording: false,
    selecting: false,
    name: '__global!tmp',
    collapsed: true,
    node: null,
  };

  toggleRecord = () => {
    if (this.state.recording) {
      window.__stopProfile();
      this.setState({ recording: false });
    } else {
      window.__startProfile();
      this.setState({ recording: true });
    }
  };

  reloadBackend = async () => {
    window.Actual.reloadBackend();
    initConnection(await global.Actual.getServerSocket());
  };

  init() {
    this.mirror = CodeMirror(this.node, {
      theme: 'monokai',
    });

    this.mirror.setSize('100%', '100%');

    // this.mirror.on('change', () => {
    //   const val = this.mirror.getValue();
    //   const [sheetName, name] = this.state.name.split('!');

    //   spreadsheet.set(sheetName, name, this.mirror.getValue());
    // });

    const mouseoverHandler = e => {
      let node = e.target;
      let cellname = null;

      while (!cellname && node) {
        cellname = node.dataset && node.dataset.cellname;
        node = node.parentNode;
      }

      if (this.state.selecting && cellname) {
        this.bind(cellname);
      }
    };
    document.body.addEventListener('mouseover', mouseoverHandler, false);

    const clickHandler = e => {
      if (this.state.selecting) {
        this.setState({ selecting: false });
      }
    };
    document.body.addEventListener('click', clickHandler, false);

    this.removeListeners = () => {
      document.body.removeEventListener('mouseover', mouseoverHandler);
      document.body.removeEventListener('click', clickHandler);
    };

    this.bind(this.state.name);
  }

  deinit() {
    if (this.unbind) {
      this.unbind();
    }

    this.removeListeners();
    this.mirror = null;
  }

  bind(resolvedName) {
    if (this.unbind) {
      this.unbind();
    }
    const [sheetName, name] = resolvedName.split('!');
    let currentReq = Math.random();
    this.currentReq = currentReq;

    send('debugCell', { sheetName, name }).then(node => {
      if (currentReq === this.currentReq) {
        if (node._run) {
          this.mirror.setValue(node._run);
        }
        this.setState({ name: node.name, node });

        this.unbind = spreadsheet.bind(sheetName, { name }, null, node => {
          if (currentReq !== this.currentReq) {
            return;
          }

          this.setState({ node: { ...this.state.node, value: node.value } });

          this.valueNode.style.transition = 'none';
          this.valueNode.style.backgroundColor = colors.y9;
          setTimeout(() => {
            this.valueNode.style.transition = 'background-color .8s';
            this.valueNode.style.backgroundColor = 'rgba(0, 0, 0, 0)';
          }, 50);
        });
      }
    });
  }

  componentWillUnmount() {
    if (this.unbind) {
      this.unbind();
      this.unbind = null;
    }
  }

  onShow = () => {
    this.setState({ collapsed: false }, () => {
      this.init();
    });
  };

  onClose = () => {
    this.setState({ collapsed: true }, () => {
      this.deinit();
    });
  };

  onSelect = () => {
    this.setState({ selecting: true });
  };

  onNameChange = e => {
    const name = e.target.value;
    this.bind(name);
    this.setState({ name });
  };

  unselect() {
    if (this.unbind) {
      this.unbind();
      this.unbind = null;
      this.setState({ sheetName: null, name: null, node: null });
    }
  }

  render() {
    const { children } = this.props;
    const { name, node, selecting, collapsed, recording } = this.state;

    return (
      <View
        style={{
          height: '100%',
          '& .CodeMirror': { border: '1px solid ' + colors.b4 },
        }}
      >
        <div style={{ flex: 1, overflow: 'hidden' }}>{children}</div>
        <View
          className="debugger"
          style={[
            {
              position: 'fixed',
              right: 0,
              bottom: 0,
              margin: 15,
              padding: 10,
              backgroundColor: 'rgba(50, 50, 50, .85)',
              color: 'white',
              zIndex: 1000,
              flexDirection: 'row',
              alignItems: 'center',
            },
            !collapsed && {
              width: 700,
              height: 200,
            },
          ]}
        >
          {collapsed ? (
            <React.Fragment>
              <div
                className="activity"
                style={{
                  width: 10,
                  height: 10,
                  backgroundColor: '#303030',
                  marginRight: 10,
                  borderRadius: 10,
                }}
              />
              <Button onClick={this.toggleRecord} style={{ marginRight: 10 }}>
                {recording ? 'Stop' : 'Start'} Profile
              </Button>
              <Button onClick={this.reloadBackend} style={{ marginRight: 10 }}>
                Reload backend
              </Button>
              <Button onClick={this.onShow}>^</Button>
            </React.Fragment>
          ) : (
            <View style={{ flex: 1 }}>
              <View
                style={{
                  flexDirection: 'row',
                  justifyContent: 'flex-start',
                  marginBottom: 5,
                  flexShrink: 0,
                }}
              >
                <Button
                  style={{
                    backgroundColor: '#303030',
                    color: 'white',
                    padding: '2px 5px',
                    marginRight: 5,
                  }}
                  onClick={this.onClose}
                >
                  v
                </Button>
                <Button
                  style={[
                    {
                      backgroundColor: '#303030',
                      color: 'white',
                      padding: '2px 5px',
                    },
                    selecting && {
                      backgroundColor: colors.p7,
                    },
                  ]}
                  onClick={this.onSelect}
                >
                  Inspect Cell
                </Button>
              </View>
              <InlineField label="Name" style={{ flex: '0 0 auto' }}>
                <Input
                  value={name}
                  onChange={this.onNameChange}
                  style={{
                    backgroundColor: '#303030',
                    color: 'white',
                    flex: 1,
                  }}
                />
              </InlineField>
              <InlineField
                label="Expr"
                style={{ flex: 1, alignItems: 'stretch', overflow: 'hidden' }}
              >
                <div
                  style={{ flex: 1, overflow: 'hidden' }}
                  ref={n => (this.node = n)}
                />
              </InlineField>
              <InlineField
                label="Dependencies"
                labelWidth={100}
                style={{ flex: '0 0 auto' }}
              >
                <pre
                  style={{
                    backgroundColor: 'rgba(0, 0, 0, 0)',
                    height: 30,
                    overflow: 'scroll',
                  }}
                >
                  {node && JSON.stringify(node._dependencies, null, 2)}
                </pre>
              </InlineField>
              <InlineField label="Value" style={{ flex: '0 0 auto' }}>
                <div
                  style={{
                    backgroundColor: 'rgba(0, 0, 0, 0)',
                    transition: 'background-color .5s',
                    height: 30,
                    overflow: 'scroll',
                  }}
                  ref={n => (this.valueNode = n)}
                >
                  {node && JSON.stringify(node.value)}
                </div>
              </InlineField>
            </View>
          )}
        </View>
      </View>
    );
  }
}

export default Debugger;