Developing vCDAT

Flask

While I have been trying to move functionality from flask to vcs-js, there are still some critical functions provided by the flask server.

For example:

  • When loading the vcdat page, html and static files are currently served by the flask server stored in the backend.
  • Browsers are unable to query the local filesystem in detail. We use flask to provide file browser functions,
  • When editing a template, we display an example plot to demonstrate the changes. This is done with flask.

All flask functionality can be found here.

Redux

It is highly recommended that you familiarize yourself with redux via the official documentation, but I have supplied some basic information here with an emphasis on how we use Redux in vCDAT.

Connect

Note

Any time a component is wrapped by something like Redux, it can affect how a component is unit tested. In some instances, you will only need to provide some additional props. In other, more complex cases, it may be easier to export an unwrapped version for testing. Historically, these components have been named “PureComponents” in vCDAT. For example, if the component was named “Example”, we would export the wrapped “Example” as the default and the unwrapped one would look like export { Example as PureExample }

A component that uses Redux does not ‘inherit’ from Redux like you might expect. Instead, a component is ‘wrapped’ by the connect function.

export default connect(mapStateToProps, mapDispatchToProps)(Example);

Where mapStateToProps and mapDispatchToProps are functions.

mapStateToProps

mapStateToProps does exactly what it says. It will take state from redux and hand it to the component as props. The function is expected to return an object containing the bits of state we want.

const mapStateToProps = (state) => {
    return {
        selected_template: state.present.ui.selected_template,
    }
}

We could then access this via this.props.selected_template.

mapDispatchToProps

The dispatch function is what tells Redux that the store needs to change. The function should return an object where each key maps to a function.

const mapDispatchToProps = (dispatch) => {
    return {
        deselectCell: function(){
            dispatch(Actions.deselectCell())
        },
    }
}

We could then deselect a cell by calling this.props.deselectCell()

Actions

An action is simply an object that contains data describing how the store should change. Actions are always given to the store by the dispatch function that comes from Connect. We keep all of the actions that can be dispatched in the Actions.js file. The only thing required by any action is that it has a type key.

How it works

Once loaded, the redux store will initialize it’s state. It does this by calling getInitialState() on each model. Once initialized, Redux follows a very simple pattern.

When something in the store (The top level object that holds everything redux knows about our application) needs to be updated, a component will dispatch an ‘Action’. This ‘Action’ essentially boils down to a javascript object with a ‘type’ key. Redux then looks to hand this action to every reducer it knows about. We identify the reducers inside the Store.js file by pointing to the reduce function of each model. Each reduce function will look at the action type to determine what, if anything, needs to be done.

Let’s look at an example. Here is what happens when a user clicks a cell to select it.

Inside the Cell component the following code segment runs in response to the browser event.

selectCell(){
    let id = this.getOwnCellId()
    if(this.props.selected_cell_id == id){
        // this.props.deselectCell() // if a cell is selected, a user clicking on it should deselect it.
        // Turning this feature off since a user manipulating an interactive plot toggles the selection too much
        return
    }
    else{
        this.props.selectCell(id)
    }
}

What is important to look at here is this.props.selectCell(id). This function actually comes from the way we connect this component to Redux.

Look further down and you will see that selectCell comes from the mapDispatchToProps function:

const mapDispatchToProps = (dispatch) => {
    return {
        selectCell: function(cell_id){
            dispatch(Actions.selectCell(cell_id))
        },
    }
}

The action that is ‘dispatched’ comes from the Actions.js file. This file simply gives us a single reliable place to reuse our actions, and helps assure that the formatting of these objects is consistent.

Here is what this action does:

selectCell(cell_id) {
    return {
        type: "SELECT_CELL",
        cell_id: cell_id
    };
},

Remember, an action is really just an object

When dispatch() is called with an action. Every reducer is given the action for processing. In this case, only the Spreadhseet model will make any changes to the store. This is because each reducer looks at the type of the action and compares that with the types that it handles

Inside the Spreadsheet model we see that it indeed handles an action of type SELECT_CELL

Once that completes, our store will finish updating and any component that relied on selected_cell_id will recieve the new value.

Vcs-js

Kitware has provided some basic documentation here.

Vcs-js is included via a script tag inside the index.html file. Vcs-js provides it’s own server with which the vcs object maintains a websocket connection to.

If you need to debug or develop a new feature for Vcs-js, the following is a quick run down of how a call to the vcs variable is processed.

All functions that the vcs object exposes are listed in the src/index.js file, and in addition to being defined here, each must be exported at the bottom of the file.

Notice that each operation makes a function call with a string value such as 'vcs.createtemplate'.

Each of these strings corresponds to a function in the vcs-server directory which has been decorated with the same string. 'vcs.createtemplate' corresponds to the function decorated like this:

@exportRpc('vcs.createtemplate')
def somefunction(self, arugment, other_argument):
    ...

In this case, that function lives in Vizualizer.py. In order to write a new function that vCDAT can call to do operations in Vcs-js, simply fill out the following:

  1. Add a new function in src/index.js
  2. Export that function at the bottom of src/index.js
  3. Create a new function in the vcs-server directory and decorate it with the same string used in step 1.
  4. Run python setup.py install using the same conda env as vCDAT
  5. Test with vCDAT (You will need to restart any instances if they were already running)

Since vcs is global you can actually call it from the dev tools directly.