Skip to content
This repository has been archived by the owner on May 11, 2021. It is now read-only.

Tutorial: Adding buttons to the Results page

Kevin Murphy edited this page Oct 22, 2015 · 1 revision

On the off-chance that it helps someone, here is a hack of the Results page that adds three buttons to the left of the main blue buttons, like so:

Note the three new buttons in a group in the middle

Whoops, nice CSS, Kevin.

The buttons manipulate server-side state by hitting the endpoint /api/state/results_mode/; I don't provide the server-side Python code here.

The UI portion is implemented in a js extension of the standard Cilantro ResultsWorkflow, and an accompanying HTML template:

  • static/scripts/javascript/src/main.js (see below)
  • static/scripts/javascript/src/ui/workflows/results.js (see below)
  • static/scripts/javascript/src/templates/workflows/results.html (see below)

The state is held in a simple Backbone model:

  • static/scripts/javascript/src/models.js (see below)

That will work in development. For r.js optimization, I had to add:

  • static/scripts/javascript/app.build.js (see below)
  • static/scripts/javascript/tpl.js (stock)
static/scripts/javascript/src/main.js

This is a lightly modified copy of Cilantro's own main.js. I don't like duplicating Cilantro's code and suspect that there is a better way.

require({
  shim: {
    'bootstrap': ['jquery'],
    'marionette': {
      deps: ['backbone'],
      exports: 'Marionette'
    },
    'highcharts': {
      deps: ['jquery'],
      exports: 'Highcharts'
    }
  }
}, ['jquery',
    'cilantro',
    'project/ui/workflows/results',
    'project/csrf'
], function ($, c, results) {

    // Default session options
    var options = {
        url: c.config.get('url'),
        credentials: c.config.get('credentials')
    };

    // Open the default session when Cilantro is ready
    c.ready(function() {

        // Open the default session defined in the pre-defined configuration.
        // Initialize routes once data is confirmed to be available
        c.sessions.open(options).then(function() {

            // Panels are defined in their own namespace since they shared
            // across workflows
            c.panels = {
                concept: new c.ui.ConceptPanel({
                    collection: this.data.concepts.queryable
                }),

                context: new c.ui.ContextPanel({
                    model: this.data.contexts.session
                })
            };

            c.dialogs = {
                exporter: new c.ui.ExporterDialog({
                    // TODO rename data.exporter on session
                    exporters: this.data.exporter
                }),

                columns: new c.ui.ConceptColumnsDialog({
                    view: this.data.views.session,
                    concepts: this.data.concepts.viewable
                }),

                query: new c.ui.EditQueryDialog({
                    view: this.data.views.session,
                    context: this.data.contexts.session,
                    collection: this.data.queries
                }),

                deleteQuery: new c.ui.DeleteQueryDialog()
            };

            var elements = [];

            // Render and append panels in the designated main element
            // prior to starting the session and loading the initial workflow
            // Render and append element for insertion
            $.each(c.panels, function(key, view) {
                view.render();
                elements.push(view.el);
            });

            $.each(c.dialogs, function(key, view) {
                view.render();
                elements.push(view.el);
            });

            // Set the initial HTML with all the global views
            var main = $(c.config.get('main'));
            main.append.apply(main, elements);

            c.workflows = {
                query: new c.ui.QueryWorkflow({
                    context: this.data.contexts.session,
                    concepts: this.data.concepts.queryable
                }),

                results: new results.ResultsWorkflow({
                    view: this.data.views.session,
                    results: this.data.preview
                })

            };

            // Define routes
            var routes = [{
                id: 'query',
                route: 'query/',
                view: c.workflows.query
            }, {
                id: 'results',
                route: 'results/',
                view: c.workflows.results
            }];

            // Workspace supported as of 2.1.0
            if (c.isSupported('2.1.0')) {
                c.workflows.workspace = new c.ui.WorkspaceWorkflow({
                    queries: this.data.queries,
                    context: this.data.contexts.session,
                    view: this.data.views.session,
                    public_queries: this.data.public_queries,  // jshint ignore:line
                    stats: this.data.stats
                });

                routes.push({
                    id: 'workspace',
                    route: 'workspace/',
                    view: c.workflows.workspace
                });
            }

            // Query URLs supported as of 2.2.0
            if (c.isSupported('2.2.0')) {
                c.workflows.queryload = new c.ui.QueryLoader({
                    queries: this.data.queries,
                    context: this.data.contexts.session,
                    view: this.data.views.session
                });

                routes.push({
                    id: 'query-load',
                    route: 'results/:query_id/',
                    view: c.workflows.queryload
                });
            }

            // Register routes and start the session
            this.start(routes);
        });

    });

});
static/scripts/javascript/src/ui/workflows/results.js

This extends the standard Cilantro ResultsWorkflow, i.e. the Results page.

/* global define */

define([
    'jquery',
    'underscore',
    'cilantro',
    '../../models',
    'tpl!project/templates/workflows/results.html'
], function($, _, c, models, results_tpl) {

    var ResultsWorkflow = c.ui.ResultsWorkflow.extend({
        template: results_tpl,

        ui: function() {
            var relativesBtn = '[data-action=add-relatives]';
            var triosBtn = '[data-action=just-trios]';
            var normalBtn = '[data-action=normal-results]';
            return _.extend({
                relativesBtn: relativesBtn,
                triosBtn: triosBtn,
                normalBtn: normalBtn
            }, c.ui.ResultsWorkflow.prototype.ui)
        },

        events: function () {
            return _.extend({
                'click @ui.triosBtn': 'toggleJustTrios',
                'click @ui.relativesBtn': 'toggleAddRelatives',
                'click @ui.normalBtn': 'toggleNormalResults',
            }, c.ui.ResultsWorkflow.prototype.events);
        },

        initialize: function() {
            c.ui.ResultsWorkflow.prototype.initialize.call(this);
            this.resultsMode = new models.ResultsModeModel();
            this.resultsMode.url = c.session.get('url') + 'state/results_mode/';
            _.bindAll(this, 'onResultsModeFetched');
        },

        onRender: function() {
            c.ui.ResultsWorkflow.prototype.onRender.call(this);
            this.resultsMode.fetch({success: this.onResultsModeFetched});
            this.pullUpLoadingOverlay();
        },

        deactivateButton: function(btn) {
            btn.removeClass('btn-info');
        },

        activateButton: function(btn) {
            var btnString = btn.attr('data-action');
            console.log('activating button: ' + btnString);
            btn.addClass('btn-info');
            // Implement mutual exclusion of buttons by hand; bootstrap not working, maybe stomped by google theme?
            if (btnString == 'add-relatives') {
                this.deactivateButton(this.ui.triosBtn);
                this.deactivateButton(this.ui.normalBtn);
            } else if (btnString == 'just-trios') {
                this.deactivateButton(this.ui.relativesBtn);
                this.deactivateButton(this.ui.normalBtn);
            } else if (btnString == 'normal-results') {
                this.deactivateButton(this.ui.triosBtn);
                this.deactivateButton(this.ui.relativesBtn);
            }
        },

        pullUpLoadingOverlay: function() {
            // Make sure loading overlay is on top of my buttons
            $('.loading-overlay').css('z-index', 9999);
        },

        onResultsModeFetched: function(model, response, options) {
            var state = model.get('results_mode');
            console.log("onResultsModeFetched: state = " + state);
            if (state == 'normal-results') {
                this.activateButton(this.ui.normalBtn);
            } else if (state == 'just-trios') {
                this.activateButton(this.ui.triosBtn);
                this.ui.triosBtn.addClass('active');
            } else if (state == 'add-relatives') {
                this.activateButton(this.ui.relativesBtn);
            }
        },

        saveResultsMode: function() {
            this.resultsMode.save(null, {
                success: function(model, response) {
                    c.data.preview.markAsDirty();
                }
            });
        },

        toggleJustTrios: function() {
            console.log("Toggle just-trios");
            if (this.resultsMode.get('results_mode') != 'just-trios') {
                this.activateButton(this.ui.triosBtn);
                this.resultsMode.set('results_mode', 'just-trios');
                this.saveResultsMode();
            }
        },

        toggleAddRelatives: function() {
            console.log("Toggle add-relatives");
            if (this.resultsMode.get('results_mode') != 'add-relatives') {
                this.activateButton(this.ui.relativesBtn);
                this.resultsMode.set('results_mode', 'add-relatives');
                this.saveResultsMode();
            }
        },

        toggleNormalResults: function() {
            console.log("Toggle normal-results");
            if (this.resultsMode.get('results_mode') != 'normal-results') {
                this.activateButton(this.ui.normalBtn);
                this.resultsMode.set('results_mode', 'normal-results');
                this.saveResultsMode();
            }
        }

    });

    return {
        ResultsWorkflow: ResultsWorkflow
    };

});
static/scripts/javascript/src/templates/workflows/results.html
<div class='navbar navbar-masthead results-workflow-navbar'>
    <div class='navbar-inner'>
        <div class='navbar-text pull-right'>
            <span class='paginator-region'></span>
            <div class="btn-group" role="group" data-toggle="buttons-radio">
              <button data-action="normal-results" type="button" class="btn btn-mini">Original</button>
              <button data-action="add-relatives" type="button" class="btn btn-mini">Add All Relatives</button>
              <button data-action="just-trios" type="button" class="btn btn-mini">Trio View</button>
            </div>

            <button data-toggle=columns-dialog class='btn btn-primary btn-mini' title='Change Columns'>
                <i class=icon-columns></i> <span class=large-display-button-text>Change Columns...</span>
            </button>
            <button data-toggle=exporter-dialog class='btn btn-primary btn-mini' title='Export Data'>
                <i class=icon-file></i> <span class=large-display-button-text>Export...</span>
            </button>
            <button data-toggle=query-dialog class='btn btn-primary btn-mini' title='Save/Share Query'>
                <i class=icon-save></i> <span class=large-display-button-text>Save/Share Query...</span>
            </button>
            <button data-toggle=context-panel class='btn btn-primary btn-mini expand-collapse' title='Hide Filter Panel'>
                <i class=icon-expand-alt></i> <span class=large-display-button-text>Hide Filters</span>
            </button>
        </div>
        <span class='navbar-text count-region'></span>
    </div>
</div>

<div class='loading-overlay hide'>
    <h4><i class='icon-spinner icon-spin'></i> Loading Results</h4>
</div>

<div class=table-region></div>
static/scripts/javascript/src/models.js
/* global define */
define([
    'backbone'
], function(Backbone) {
    var ResultsModeModel = Backbone.Model.extend({
        idAttribute: 'results_mode'
    });
    return {ResultsModeModel: ResultsModeModel};
});
static/scripts/javascript/app.build.js
// RequireJS optimization configuration
// Full example: https://github.com/jrburke/r.js/blob/master/build/example.build.js

({
    // Optimize relative to this url (i.e. the current directory)
    baseUrl: '.',

    // The source directory of the modules
    appDir: 'src',

    // The target directory of the optimized modules
    dir: 'min',

    optimize: 'uglify',

    optimizeCss: 'none',

    paths: {
        'project': '.',
        'cilantro': 'empty:',
        'jquery': 'empty:',
        'underscore': 'empty:',
        'backbone': 'empty:',
        'tpl': '../tpl'
    },

    name: 'main'
})