Commit e2faaf73 authored by David Hoksza's avatar David Hoksza
Browse files

initial commit

parents
# Minerva plugin creation with starter-kit
Minerva plugin is a javascript file which manages its dedicated space in the Minerva page (right hand side panel). Since the plugin is a single Javascript file, all its content needs to be added/updated with JavaScript (no HTML pages). The same holds for CSS which either needs to be added via JavaSscript, or bundled using NPM (that is what starter-kit is doing), Gulp, Webpack or other similar technologies.
## General comments
* Minerva uses jQuery so plugins can use it as well since it is loaded in the global scope
* Minerva uses Bootstrap so Bootstrap styles are available to plugins as well
* Many of the functions which are used to interact with Minerva are asynchronous and thus return a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) and not the actual data. This holds mainly for functions for data retrieval such as `getAllBioentities` (see below).
## Building the starter-kit
The starter-kit example uses NPM (node package manager) and Browserify (and few other packages such as browserify-css or uglify) to build the plugin distriubtion file which you can publish at a URL address which can then be referenced from Minerva. To build the starter-kit you need to do the following steps:
* Download the starter-kit
* Download and install [NPM](https://nodejs.org/en/download/) if it is not yet installed on your system (try running `npm` from command line to find out)
* run `npm install` (this will create the *node_modules* directory with the required NPM modules)
* run `npm run build` to build the plugin which will be available in the *dist* directory
* publish the resulting *plugin.js* file somewhere where it can be accessed by Minerva (please beware that if the instance is running on HTTPS, the plugin must be also accessible through HTTPS)
For the development process you might want to have your local instance of Minerva running, so that you do not have to publish the plugin file every time you do a change. However, if you do not have a local Minerva instance, you can use command line to commit and push your plugin to, e.g., GitHub and provide Minerva address of the raw file. So for example if your GitHub repository with the plugin is [https://github.com/davidhoksza/minerva-plugins-starter-kit/](https://github.com/davidhoksza/minerva-plugins-starter-kit/) you can run
```
npm run build && git commit -m "distribution commit" dist/plugin.js && git push
```
which will build your plugin and commit and push it to the Git repository (GitHub in our case) and your plugin will be available at [https://raw.githubusercontent.com/davidhoksza/minerva-plugins-starter-kit/master/dist/plugin.js](https://raw.githubusercontent.com/davidhoksza/minerva-plugins-starter-kit/master/dist/plugin.js)
## Plugin structure
The starter-kit contains CSS with styles sheets and JavaScript code. The kit actually uses SCSS ([Sass](https://sass-lang.com/) extension of CSS) which is then compiled into CSS during the build process. The JavaScript code consists of a single `index.js` file which:
* Registers required functions with Minerva
* Creates plugin's HTML structure
* Interacts with Minerva to do what needs to be done
#### Registering with Minerva
When the plugin is loaded into Minerva `minervaDefine` function is called, so this needs to be present in every plugin script. This function needs to return an object which maps keys `register`, `unregister`, `getName` and `getVersion` to the corresponding functions. Only the `register` function is passed an argument being the Minerva object which can then later be used to interact with the map.
#### Creating plugin's HTML structure
The Minerva object passed to the `register` function contains the `element` attribute being a jQuery object corresponding to the DOM element holding the plugin container (Minerva uses jQuery, so plugins do not need to include it). With the container element in hand, the plugin can add and modify its content freely. Of course, the plugin can also modify Minerva's DOM elements, however we strongly discourage from that.
#### Interacting with Minerva
###### Minerva proxy object
All the interaction with Minerva should happen through the minerva proxy object or ServerConnector (see next section) passed to the `register` function. To explore this object, starter-kit logs it into the console (`console.log('minerva object ', minervaProxy);`) so after the plugin is loaded, you can check out your browser's developers console and go through it. The structure of the object is following (not all attributes are mentioned):
* configuration: includes information about available types of elements, reactions, miriam types, configuration options, map types and so on
* project: content-related data and functionaliry
* data: functions to retrieve the data, mainly `getAllBioEntities` and `getBioEntityById`
* beware that most of these functions are asynchronous so they actually return a promise not the actual objects
* map: functions to interact with the visual aspect of the map, mainly `showBioEntity` (highlights a bioentity), `hideBioEntity`, `fitBounds` (zooms to provided coordinates) and `addListener` (enables listening to events such as selection of entities) - see examples of using these functions in the starter-kit and the Minerva's [JavaScript API documntation](https://git-r3lab.uni.lu/piotr.gawron/minerva#javascript-api-unstable-dev-api).
Some of the functions are also described in the [JavaScript API documntation](https://git-r3lab.uni.lu/piotr.gawron/minerva#javascript-api-unstable-dev-api).
###### ServerConnector object
Minerva also exposes variable called `ServerConnector` to the global scope (therefore you can explore it by typing `ServerConnector` in the browser developers console). It provides various functionality such as ability to retrieve list of models, overlays, projects, link to logo file, server address or add and modify comments, users.
###### Minerva's API
It can happen that there exists a (mainly data-related) functionality which is not available in the proxy object but is available through [Minerva's REST API](https://git-r3lab.uni.lu/piotr.gawron/minerva). In such case you can use Ajax to retrieve the data (the easiest way is probably to use jQuery's [getJSON](http://api.jquery.com/jquery.getjson/) function).
!function r(a,l,d){function s(t,e){if(!l[t]){if(!a[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(c)return c(t,!0);var o=new Error("Cannot find module '"+t+"'");throw o.code="MODULE_NOT_FOUND",o}var i=l[t]={exports:{}};a[t][0].call(i.exports,function(e){var n=a[t][1][e];return s(n||e)},i,i.exports,r,a,l,d)}return l[t].exports}for(var c="function"==typeof require&&require,e=0;e<d.length;e++)s(d[e]);return s}({1:[function(e,n,t){"use strict";var i=[],a=function(e,n){var t=document.head||document.getElementsByTagName("head")[0],o=i[i.length-1];if((n=n||{}).insertAt=n.insertAt||"bottom","top"===n.insertAt)o?o.nextSibling?t.insertBefore(e,o.nextSibling):t.appendChild(e):t.insertBefore(e,t.firstChild),i.push(e);else{if("bottom"!==n.insertAt)throw new Error("Invalid value for parameter 'insertAt'. Must be 'top' or 'bottom'.");t.appendChild(e)}};n.exports={createLink:function(e,n){var t=document.head||document.getElementsByTagName("head")[0],o=document.createElement("link");for(var i in o.href=e,o.rel="stylesheet",n)if(n.hasOwnProperty(i)){var r=n[i];o.setAttribute("data-"+i,r)}t.appendChild(o)},createStyle:function(e,n,t){t=t||{};var o=document.createElement("style");for(var i in o.type="text/css",n)if(n.hasOwnProperty(i)){var r=n[i];o.setAttribute("data-"+i,r)}o.sheet?(o.innerHTML=e,o.sheet.cssText=e,a(o,{insertAt:t.insertAt})):o.styleSheet?(a(o,{insertAt:t.insertAt}),o.styleSheet.cssText=e):(o.appendChild(document.createTextNode(e)),a(o,{insertAt:t.insertAt}))}}},{}],2:[function(e,n,t){var o=".starter-kit-container {\n padding: 10px;\n background-color: #bada55;\n}\n.btn-uniprot,\n.btn-pick-random,\n.btn-minerva {\n margin-bottom: 5px;\n}\n";e("browserify-css").createStyle(o,{href:"src\\css\\styles.css"},{insertAt:"bottom"}),n.exports=o},{"browserify-css":1}],3:[function(e,n,t){"use strict";e("../css/styles.css");var o="starter-kit",i="https://minerva-dev.lcsb.uni.lu/minerva-proxy/",r={selected:[],allBioEntities:[],pickedRandomly:void 0},a=void 0,l=void 0,d=function(e){var n;console.log("registering "+o+" plugin"),$(".tab-content").css("position","relative"),a=e,l=$(a.element),l.attr("id"),console.log("minerva object ",a),console.log("project id: ",a.project.data.getProjectId()),console.log("model id: ",a.project.data.getModels()[0].modelId),a.project.map.addListener({dbOverlayName:"search",type:"onSearch",callback:u}),(n=$('<div class="'+o+'-container"></div>').appendTo(l)).append('\n <div class="panel panel-default panel-events">\n <div class="panel-heading">Events (Select an element in the map)</div>\n <div class="panel-body"> \n </div>\n </div>\n '),n.append('<button type="button" class="btn-focus btn btn-primary btn-default btn-block">Focus</button>'),n.append('<button type="button" class="btn-highlight btn btn-primary btn-default btn-block">Highlight</button>'),n.append("<hr>"),n.append('<button type="button" class="btn-pick-random btn btn-primary btn-default btn-block">Retrieve random object from map</button>'),n.append('\n <div class="panel panel-default panel-randomly-picked">\n <div class="panel-heading">Randomly picked object</div>\n <div class="panel-body"> \n </div>\n </div>\n '),n.append('<button type="button" class="btn-focus-random btn btn-primary btn-default btn-block">Focus</button>'),n.append('<button type="button" class="btn-highlight-random btn btn-primary btn-default btn-block">Highlight</button>'),n.append("<hr>"),n.append("<h4>Query UniProt API</h4>"),n.append('<button type="button" class="btn-uniprot btn btn-primary btn-default btn-block" title="Queries UniProt using the element selected from the map">Retrieve from UniProt</button>'),n.append('\n <div class="panel panel-default panel-uniprot">\n <div class="panel-heading">Uniprot records for the selected element</div>\n <div class="panel-body">\n <code></code>\n </div>\n </div>\n '),n.append("<hr>"),n.append("<h4>Query Minerva API</h4>"),n.append('\n <form class="form-horizontal">\n <div class="form-group">\n <label class="col-sm-2 control-label">Address</label>\n <div class="col-sm-10">\n <input class="input-minerva-address form-control" value="https://minerva-dev.lcsb.uni.lu/minerva">\n </div>\n </div>\n <div class="form-group">\n <label class="col-sm-2 control-label">Project ID</label>\n <div class="col-sm-10">\n <input class="input-minerva-projectid form-control" value="sample2">\n </div>\n </div> \n </form>\n <button type="button" class="btn-minerva btn btn-primary btn-default btn-block">Retrieve from Minerva</button>\n <div class="panel panel-default panel-minerva">\n <div class="panel-heading">Names of elements</div>\n <div class="panel-body"> \n </div>\n </div>\n '),n.find(".btn-highlight").on("click",function(){return m()}),n.find(".btn-focus").on("click",function(){return b()}),n.find(".btn-pick-random").on("click",function(){return function(){function n(){r.pickedRandomly=r.allBioEntities[Math.floor(Math.random()*r.allBioEntities.length)];var e=r.pickedRandomly.constructor.name+" - ";"Alias"===r.pickedRandomly.constructor.name?e+=r.pickedRandomly.getElementId()+" - "+r.pickedRandomly.getName():e+=""+r.pickedRandomly.getReactionId(),l.find(".panel-randomly-picked .panel-body").html(e)}0<r.allBioEntities.length?n():a.project.data.getAllBioEntities().then(function(e){r.allBioEntities=e,n()})}()}),n.find(".btn-highlight-random").on("click",function(){return m(!0)}),n.find(".btn-focus-random").on("click",function(){return b(!0)}),n.find(".btn-uniprot").on("click",function(){return e=(e=l.find(".panel-events .panel-body").text()).substring(0,e.indexOf(" - ")),console.log(e),void $.ajax({type:"GET",url:"https://www.uniprot.org/uniprot/?query="+e+"&sort=score&columns=id,entry%20name,reviewed,protein%20names,3d,genes,organism,length&format=tab&limit=10"}).then(function(e){l.find(".panel-uniprot .panel-body code").text(e)});var e}),n.find(".btn-minerva").on("click",function(){return t=l.find(".input-minerva-address").val(),o=l.find(".input-minerva-projectid").val(),void $.ajax({type:"GET",url:i+"?url="+t+"/api/projects/"+o+"/models/",dataType:"json"}).then(function(e){console.log("Retrived models from "+i,e);var n=e[0].idObject;return $.ajax({type:"GET",url:i+"?url="+t+"/api/projects/"+o+"/models/"+n+"/bioEntities/elements/",dataType:"json"})}).then(function(e){console.log("Retrived elements from "+i,e);var n="";e.forEach(function(e){n+=e.name+"<br/>"}),l.find(".panel-minerva .panel-body").html(n)});var t,o})},s=function(){return console.log("unregistering "+o+" plugin"),a.project.map.removeAllListeners(),a.project.map.getHighlightedBioEntities().then(function(e){return a.project.map.hideBioEntity(e)})},c=function(){return o},p=function(){return"1.0.0"};function u(e){r.selected=e[0];var n="";0<r.selected.length&&r.selected.forEach(function(e){"Alias"===e.constructor.name&&(n+="<div>"+e.getName()+" - "+e.getElementId()+"</div>")}),l.find(".panel-events .panel-body").html(n)}function m(){var e=0<arguments.length&&void 0!==arguments[0]&&arguments[0],n=[];e?r.pickedRandomly&&n.push({element:{id:r.pickedRandomly.id,modelId:r.pickedRandomly.getModelId(),type:r.pickedRandomly.constructor.name.toUpperCase()},type:"SURFACE",options:{color:"#00FF00",opacity:.5}}):r.selected.forEach(function(e){n.push({element:{id:e.id,modelId:e.getModelId(),type:"ALIAS"},type:"ICON"})}),a.project.map.showBioEntity(n)}function b(){var e=0<arguments.length&&void 0!==arguments[0]&&arguments[0];function n(e){"Alias"===e.constructor.name?a.project.map.fitBounds({modelId:e.getModelId(),x1:e.getX(),y1:e.getY(),x2:e.getX()+e.getWidth(),y2:e.getY()+e.getHeight()}):a.project.map.fitBounds({modelId:e.getModelId(),x1:e.getCenter().x,y1:e.getCenter().y,x2:e.getCenter().x,y2:e.getCenter().y})}!e&&0<r.selected.length&&n(r.selected[0]),e&&r.pickedRandomly&&n(r.pickedRandomly)}minervaDefine(function(){return{register:d,unregister:s,getName:c,getVersion:p}})},{"../css/styles.css":2}]},{},[3]);
{
"name": "minerva-plugins-starter-kit",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build-css": "node-sass --include-path src/css src/css/styles.scss src/css/styles.css",
"build": "mkdirp dist && npm run build-css && browserify -t [ babelify --presets es2015 ] -t browserify-css src/js/index.js | uglifyjs --compress --mangle > dist/plugin.js ",
"watch-js": "mkdirp dist && npm run build-css && watchify -v -t [ babelify --presets es2015 ] -t browserify-css src/js/index.js -o dist/plugin.js",
"clean": "rimraf dist"
},
"author": "David Hoksza",
"license": "MIT",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babelify": "^8.0.0",
"browserify": "^14.5.0",
"browserify-css": "^0.13.1",
"mkdirp": "^0.5.1",
"node-sass": "^4.6.1",
"rimraf": "latest",
"uglify-js": "^3.1.9",
"watchify": "^3.9.0"
}
}
$badass: #bada55;
$pluginName: starter-kit;
.#{$pluginName}-container {
padding: 10px;
background-color: $badass;
}
.btn-uniprot, .btn-pick-random, .btn-minerva {
margin-bottom: 5px;
}
require('../css/styles.css');
const pluginName = 'starter-kit';
const pluginVersion = '1.0.1';
const minervaProxyServer = 'https://minerva-dev.lcsb.uni.lu/minerva-proxy/';
const globals = {
selected: [],
allBioEntities: [],
pickedRandomly: undefined
};
// ******************************************************************************
// ********************* PLUGIN REGISTRATION WITH MINERVA *********************
// ******************************************************************************
let minervaProxy;
let pluginContainer;
let pluginContainerId;
const register = function(_minerva) {
console.log('registering ' + pluginName + ' plugin');
$(".tab-content").css('position', 'relative');
minervaProxy = _minerva;
pluginContainer = $(minervaProxy.element);
pluginContainerId = pluginContainer.attr('id');
console.log('minerva object ', minervaProxy);
console.log('project id: ', minervaProxy.project.data.getProjectId());
console.log('model id: ', minervaProxy.project.data.getModels()[0].modelId);
initPlugin();
};
const unregister = function () {
console.log('unregistering ' + pluginName + ' plugin');
unregisterListeners();
return deHighlightAll();
};
const getName = function() {
return pluginName;
};
const getVersion = function() {
return pluginVersion;
};
/**
* Function provided by Minerva to register the plugin
*/
minervaDefine(function (){
return {
register: register,
unregister: unregister,
getName: getName,
getVersion: getVersion
}
});
function initPlugin () {
registerListeners();
initMainPageStructure();
}
function registerListeners(){
minervaProxy.project.map.addListener({
dbOverlayName: "search",
type: "onSearch",
callback: searchListener
});
}
function unregisterListeners() {
minervaProxy.project.map.removeAllListeners();
}
// ****************************************************************************
// ********************* MINERVA INTERACTION*********************
// ****************************************************************************
function deHighlightAll(){
return minervaProxy.project.map.getHighlightedBioEntities().then( highlighted => minervaProxy.project.map.hideBioEntity(highlighted) );
}
// ****************************************************************************
// ********************* PLUGIN STRUCTURE AND INTERACTION*********************
// ****************************************************************************
function initMainPageStructure(){
const container = $('<div class="' + pluginName + '-container"></div>').appendTo(pluginContainer);
container.append(`
<div class="panel panel-default panel-events">
<div class="panel-heading">Events (Select an element in the map)</div>
<div class="panel-body">
</div>
</div>
`);
container.append('<button type="button" class="btn-focus btn btn-primary btn-default btn-block">Focus</button>');
container.append('<button type="button" class="btn-highlight btn btn-primary btn-default btn-block">Highlight</button>');
container.append('<hr>');
container.append('<button type="button" class="btn-pick-random btn btn-primary btn-default btn-block">Retrieve random object from map</button>');
container.append(`
<div class="panel panel-default panel-randomly-picked">
<div class="panel-heading">Randomly picked object</div>
<div class="panel-body">
</div>
</div>
`);
container.append('<button type="button" class="btn-focus-random btn btn-primary btn-default btn-block">Focus</button>');
container.append('<button type="button" class="btn-highlight-random btn btn-primary btn-default btn-block">Highlight</button>');
container.append('<hr>');
container.append('<h4>Query UniProt API</h4>');
container.append('<button type="button" class="btn-uniprot btn btn-primary btn-default btn-block" ' +
'title="Queries UniProt using the element selected from the map">Retrieve from UniProt</button>');
container.append(`
<div class="panel panel-default panel-uniprot">
<div class="panel-heading">Uniprot records for the selected element</div>
<div class="panel-body">
<code></code>
</div>
</div>
`);
container.append('<hr>');
container.append('<h4>Query Minerva API</h4>');
container.append(`
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label">Address</label>
<div class="col-sm-10">
<input class="input-minerva-address form-control" value="https://minerva-dev.lcsb.uni.lu/minerva">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Project ID</label>
<div class="col-sm-10">
<input class="input-minerva-projectid form-control" value="sample2">
</div>
</div>
</form>
<button type="button" class="btn-minerva btn btn-primary btn-default btn-block">Retrieve from Minerva</button>
<div class="panel panel-default panel-minerva">
<div class="panel-heading">Names of elements</div>
<div class="panel-body">
</div>
</div>
`);
container.find('.btn-highlight').on('click', () => highlightSelected() );
container.find('.btn-focus').on('click', () => focusOnSelected() );
container.find('.btn-pick-random').on('click', () => pickRandom() );
container.find('.btn-highlight-random').on('click', () => highlightSelected(true) );
container.find('.btn-focus-random').on('click', () => focusOnSelected(true) );
container.find('.btn-uniprot').on('click', () => retrieveUniprot() );
container.find('.btn-minerva').on('click', () => retrieveMinerva() );
}
function searchListener(entites) {
globals.selected = entites[0];
let str = '';
if (globals.selected.length > 0) {
globals.selected.forEach(e => {if (e.constructor.name === 'Alias') str += `<div>${e.getName()} - ${e.getElementId()}</div>`});
}
pluginContainer.find('.panel-events .panel-body').html(str);
}
function pickRandom() {
function pick(){
globals.pickedRandomly = globals.allBioEntities[Math.floor(Math.random() * globals.allBioEntities.length)];
let html = `${globals.pickedRandomly.constructor.name} - `;
if (globals.pickedRandomly.constructor.name === 'Alias') html += `${globals.pickedRandomly.getElementId()} - ${globals.pickedRandomly.getName()}`;
else html += `${globals.pickedRandomly.getReactionId()}`;
pluginContainer.find('.panel-randomly-picked .panel-body').html(html);
}
if (globals.allBioEntities.length > 0) {
pick();
}
else {
minervaProxy.project.data.getAllBioEntities().then(function(bioEntities) {
globals.allBioEntities = bioEntities;
pick();
});
}
}
function highlightSelected(pickedRandomly = false) {
const highlightDefs = [];
if (pickedRandomly) {
if (globals.pickedRandomly) {
highlightDefs.push({
element: {
id: globals.pickedRandomly.id,
modelId: globals.pickedRandomly.getModelId(),
type: globals.pickedRandomly.constructor.name.toUpperCase()
},
type: "SURFACE",
options: {
color: '#00FF00',
opacity: 0.5
}
});
}
} else {
globals.selected.forEach(e => {
highlightDefs.push({
element: {
id: e.id,
modelId: e.getModelId(),
type: "ALIAS"
},
type: "ICON"
});
});
}
minervaProxy.project.map.showBioEntity(highlightDefs);
}
function focusOnSelected(pickedRandomly = false) {
function focus(entity) {
if (entity.constructor.name === 'Alias') {
minervaProxy.project.map.fitBounds({
modelId: entity.getModelId(),
x1: entity.getX(),
y1: entity.getY(),
x2: entity.getX() + entity.getWidth(),
y2: entity.getY() + entity.getHeight()
});
} else {
minervaProxy.project.map.fitBounds({
modelId: entity.getModelId(),
x1: entity.getCenter().x,
y1: entity.getCenter().y,
x2: entity.getCenter().x,
y2: entity.getCenter().y
});
}
}
if (!pickedRandomly && globals.selected.length > 0) focus(globals.selected[0]);
if (pickedRandomly && globals.pickedRandomly) focus(globals.pickedRandomly);
}
function retrieveUniprot() {
let query = pluginContainer.find('.panel-events .panel-body').text();
query = query.substring(0,query.indexOf(' - '));
console.log(query);
$.ajax({
type: 'GET',
url: 'https://www.uniprot.org/uniprot/?query=' + query + '&sort=score&columns=id,entry%20name,reviewed,protein%20names,3d,genes,organism,length&format=tab&limit=10'
}).then(function(result){
pluginContainer.find('.panel-uniprot .panel-body code').text(result);
})
}
function retrieveMinerva() {
const address = pluginContainer.find('.input-minerva-address').val();
const projectId = pluginContainer.find('.input-minerva-projectid').val();
$.ajax({
type: 'GET',
url: `${minervaProxyServer}?url=${address}/api/projects/${projectId}/models/`,
dataType: 'json'
}).then((models) => {
console.log(`Retrived models from ${minervaProxyServer}`, models);
const firstModelId = models[0].idObject;
return $.ajax({
type: 'GET',
url: `${minervaProxyServer}?url=${address}/api/projects/${projectId}/models/${firstModelId}/bioEntities/elements/`,
dataType: 'json'
});
}).then((elements) => {
console.log(`Retrived elements from ${minervaProxyServer}`, elements);
let names = '';
elements.forEach(function (element) {
names += element.name + '<br/>';
});
pluginContainer.find('.panel-minerva .panel-body').html(names);
});
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment