diff --git a/.gitignore b/.gitignore index f146193d1..ac85d74fb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,10 @@ /resources/public/js/min /resources/public/vendor /resources/public/css -/resources/public/tools +/resources/public/tools/* +/tools/bluegenes** config/**/*.edn +!config/defaults/config.edn .lein-failures out .idea @@ -16,4 +18,4 @@ node_modules .nrepl-port .DS_Store pom.xml -package-lock.json \ No newline at end of file +package-lock.json diff --git a/OldDockerfile b/OldDockerfile deleted file mode 100644 index 025f97508..000000000 --- a/OldDockerfile +++ /dev/null @@ -1,67 +0,0 @@ -FROM node:argon -CMD ["node"] - -ENV LEIN_VERSION=2.7.0 -ENV LEIN_INSTALL=/usr/local/bin/ - -WORKDIR /tmp - -RUN \ - apt-get update && \ - apt-get install -y default-jdk - -# Define working directory. -WORKDIR /data - -# Define commonly used JAVA_HOME variable -ENV JAVA_HOME /usr/lib/jvm/java-8-oracle - -WORKDIR /tmp - -# Download the whole repo as an archive -RUN mkdir -p $LEIN_INSTALL \ - && wget --quiet https://github.com/technomancy/leiningen/archive/$LEIN_VERSION.tar.gz \ - && echo "Comparing archive checksum ..." \ - && echo "b4624548ada176c1d122dd9867a1bed09706fcd0 *$LEIN_VERSION.tar.gz" | sha1sum -c - \ - - && mkdir ./leiningen \ - && tar -xzf $LEIN_VERSION.tar.gz -C ./leiningen/ --strip-components=1 \ - && mv leiningen/bin/lein-pkg $LEIN_INSTALL/lein \ - && rm -rf $LEIN_VERSION.tar.gz ./leiningen \ - - && chmod 0755 $LEIN_INSTALL/lein \ - -# Download and verify Lein stand-alone jar - && wget --quiet https://github.com/technomancy/leiningen/releases/download/$LEIN_VERSION/leiningen-$LEIN_VERSION-standalone.zip \ - && wget --quiet https://github.com/technomancy/leiningen/releases/download/$LEIN_VERSION/leiningen-$LEIN_VERSION-standalone.zip.asc \ - - && gpg --keyserver pool.sks-keyservers.net --recv-key 2E708FB2FCECA07FF8184E275A92E04305696D78 \ - && echo "Verifying Jar file signature ..." \ - && gpg --verify leiningen-$LEIN_VERSION-standalone.zip.asc \ - -# Put the jar where lein script expects - && rm leiningen-$LEIN_VERSION-standalone.zip.asc \ - && mv leiningen-$LEIN_VERSION-standalone.zip /usr/share/java/leiningen-$LEIN_VERSION-standalone.jar \ - -# Some REPLs (e.g., Figwheel) necessitate a readline wrapper. - && apt-get update && apt-get install rlfe && rm -rf /var/lib/apt/lists/* - -ENV PATH=$PATH:$LEIN_INSTALL -ENV LEIN_ROOT 1 - -RUN lein - -WORKDIR /usr/src/app - -COPY . /usr/src/app - -WORKDIR /usr/src/app -RUN npm install -g grunt-cli -RUN npm install -g less -RUN npm install -# RUN echo '@import "../../bootstrap/less/variables.less";'> '/usr/src/app/resources/public/vendor/bootstrap-material-design/less/_import-bs-less.less' - -RUN lein clean -RUN lein less once -RUN lein cljsbuild once min -CMD lein run diff --git a/config/defaults/config.edn b/config/defaults/config.edn new file mode 100644 index 000000000..6289eb248 --- /dev/null +++ b/config/defaults/config.edn @@ -0,0 +1,4 @@ +{ + ;;where tools are installed. Make sure this is an absolute path. + :bluegenes-tool-path "/intermine/tools/node_modules" + } diff --git a/docs/README.md b/docs/README.md index c6b4c300d..893a4f0b8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,5 +8,7 @@ Documentation is generally distributed to live in-folder with the code in the fo - [Getting Started - developers](getting-started.md) - how to run bluegenes locally so you can make changes - [Building BlueGenes for production](production-builds.md) - how to launch it to servers, for production use. - [Troubleshooting BlueGenes issues](troubleshooting.md) - steps for when the computer says 'no'. -- [Configuring BlueGenes](configuring-bluegenes.md) - includes how to configure Analytics and which mines are shown. - - [Server-side config](https://github.com/intermine/bluegenes/blob/dev/config/dev/README.md) - such as port number, default intermine url. +- [Configuring BlueGenes](configuring-bluegenes.md) - includes how to configure Analytics and which mines are shown. + - [Server-side config](https://github.com/intermine/bluegenes/blob/dev/config/dev/README.md) - such as port number, default intermine url. + +- [Tool API](https://github.com/intermine/bluegenes/blob/dev/src/clj/bluegenes/ws/tools.md) - configuring and installing tools diff --git a/docs/getting-started.md b/docs/getting-started.md index f70570ae6..2d4ccd330 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,11 +1,10 @@ -You can run bluegenes locally for development purposes. Here's what the local setup should look like. +You can run bluegenes locally for development purposes. Here's what the local setup should look like. ## System Requirements * Java 1.6+ * [Leiningen 2.5+](https://leiningen.org/) -* [node 7+][nodejs] (you can check your version using `node -v`) -* [npm][npm] -* **Required:** The InterMine you point BlueGenes at *must* be running InterMine 1.8 or later; ideally 2.0. +* [node 7+][nodejs] (you can check your version using `node -v`). We recommend installing node using [nvm](https://github.com/creationix/nvm) +* **Required:** The InterMine you point BlueGenes at *must* be running InterMine 1.8 or later; ideally 2.0. ### Download dependencies. @@ -28,7 +27,7 @@ _Or_, if you'll be making lots of style edits and don't want to type `lein less lein less auto ``` -Note that you won't see a prompt telling you when it's complete if you use `lein less auto` - but the browser page will automatically refresh so you'll know when it's done. +Note that you won't see a prompt telling you when it's complete if you use `lein less auto` - but the browser page will automatically refresh so you'll know when it's done. ### Start the process to reload code changes in the browser: @@ -56,11 +55,9 @@ The above command assumes that you have [phantomjs](https://www.npmjs.com/packag ## What next? -Once you're happy with any edits you've made, you probably want to check that it all works the same in a minified prod build. See [production builds](production-builds.md) for info on deploying and testing a minified build. +Once you're happy with any edits you've made, you probably want to check that it all works the same in a minified prod build. See [production builds](production-builds.md) for info on deploying and testing a minified build. [lein]: https://github.com/technomancy/leiningen [npm]: https://www.npmjs.com/ [nodejs]: https://nodejs.org/ - - diff --git a/less/developer.less b/less/developer.less index b84d6b430..14f42b0af 100644 --- a/less/developer.less +++ b/less/developer.less @@ -1,6 +1,8 @@ +@import "variables"; + .developer { .icon-size { - margin: 1em 0; + margin: 1*@spacer 0; } .icon-sizing-example { @@ -8,10 +10,10 @@ background-color: #eee; border-radius: 2px; border: solid 1px #ddd; - margin: 0 0.5em; + margin: 0 0.5*@spacer; .demo { - margin: 0 0.5em; + margin: 0 0.5*@spacer; display: flex; flex-direction: column-reverse; justify-content: flex-start; @@ -22,7 +24,7 @@ border-collapse: collapse; .icon { - font-size: 2em; + font-size: 2*@spacer; &.icon-venn-combine { fill: rgba(0,0,0,0); @@ -38,7 +40,7 @@ background: #eee; border-radius: 2px; border: solid 1px rgba(0, 0, 0, 0.2); - font-family: mono; + font-family: courier, mono; } td { @@ -49,4 +51,186 @@ background: rgba(0, 0, 0, 0.1); } } + + .dev-navigation { + display: flex; + flex-wrap: wrap; + margin: 0; + padding: 0; + + li { + list-style-type: none; + padding: @spacer 2*@spacer; + background-color: @highlight-color; + color: @highlight-color-text; + border-radius: 2px; + margin: @spacer; + + a { + display: flex; + align-items: center; + color: #fff; + + .icon { + fill: #fff; + width: 1.5*@spacer; + height: 1.5*@spacer; + } + } + } + } + + .tool-store { + padding: @spacer 0; + + .info { + .border-radius; + .box-shadow; + border: solid 1px @light-contrast; + display: flex; + align-items: center; + padding: @spacer 0; + background-color: @content-color-background; + + p { + margin: 0; + } + + .icon-info { + width: 3*@spacer; + height: 3*@spacer; + padding: 0.5*@spacer 0.5*@spacer 0.5*@spacer 0; + fill: @highlight-color; + } + } + + .tool-list { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + + .tool-size { + min-width: 220px; + max-width: 350px; + } + + .tool { + .tool-size; + display: flex; + flex-direction: column; + align-items: center; + margin: @spacer 0.5*@spacer *@spacer; + padding: 0; + .border-radius; + .box-shadow; + background-color: #fff; + + h2, + h3 { + font-size: 1.1 *@spacer; + display: inline-block; + margin: @spacer/5; + } + + > h2 { + background-color: @navbar-color; + color: @navbar-color-text; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + padding: 1em; + display: flex; + align-self: stretch; + margin: 0; + } + + .description { + border-bottom: solid 1px #ccc; + padding-bottom: @spacer; + + .icon-info { + margin-top: 2px; + } + } + + .tool-preview { + .tool-size; + + img { + object-fit: cover; + } + border-bottom: solid 1px @body-foreground-color; + overflow: hidden; + } + + .tool-no-preview { + border-bottom: solid 1px @body-foreground-color; + background-color: @light-contrast; + display: flex; + flex-grow: 1; + max-height: 220px; + max-width: none; + align-items: center; + justify-content: center; + width: 100%; + font-size: 1.3*@spacer; + } + + .details { + display: flex; + flex-direction: column; + font-weight: normal; + align-items: center; + padding: @spacer; + + h2 { + font-weight: bold; + } + } + + .tool-type h3 { + font-weight: bold; + } + + .tool-class { + ul { + display: flex; + flex-direction: column; + padding: 0; + } + + li { + display: flex; + background-color: transparent; + color: @body-foreground-color; + border-width: 0 0 0 3px; + padding: 0 0.5 * @spacer; + margin: @spacer/6; + } + } + } + } + } + + .mine-choice { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 10px; + grid-auto-rows: minmax(2em, auto); + + label { + padding: 2px 4px 0 2px; + color: @body-foreground-color; + + input { + margin-right: 2px; + } + + &.checked { + border-bottom: solid 3px @highlight-color; + border-radius: 2px; + background: rgba(0, 0, 0, 0.07); + color: @body-foreground-color; + } + } + } } diff --git a/less/site.less b/less/site.less index 3b2dc84d9..de922379f 100644 --- a/less/site.less +++ b/less/site.less @@ -268,31 +268,6 @@ td.skipped { color: black; pointer-events: none; } -/** input.form-control { - &:focus { - .form-group.is-focused .form-control; - } -} **/ -.mine-choice { - display: flex; - justify-content: space-between; - - label { - padding: 2px 4px 0 2px; - color: @body-foreground-color; - - input { - margin-right: 2px; - } - - &.checked { - border-bottom: solid 3px @highlight-color; - border-radius: 2px; - background: rgba(0, 0, 0, 0.07); - color: @body-foreground-color; - } - } -} .form-control, .form-group .form-control { @@ -430,15 +405,15 @@ button.btn { } .icon-2x { - .icon(2em) + .icon(2em); } .icon-3x { - .icon(3em) + .icon(3em); } .icon-4x { - .icon(4em) + .icon(4em); } .dropdown-mixed-content { diff --git a/less/variables.less b/less/variables.less index 5ddf95f97..c1f061467 100644 --- a/less/variables.less +++ b/less/variables.less @@ -1,18 +1,31 @@ @body-background-color: #f7f7f7; @body-foreground-color: #444444; @link-color: #000; + +//Bits you want to draw the eye to - usually y 1-2 items per page, pls @highlight-color: #039BE5; @highlight-color-text: #fff; @cta-color: @highlight-color; @cta-color-text: @body-background-color; + +//possibly defunct? @selectedColor: #3F51B5; @selectedColorText: @body-background-color; +@notice-me: #F57F17; + +//for stripy tables @stripe-color: #f6f6f6; @light-contrast: #e2e2e2; -@notice-me: #F57F17; + @navbar-color: #505050; @navbar-color-text: #fff; +//for pages ares that need to be brought forward from the background. Assume same colour text as normal. +@content-color-background: #fff; +@content-color-text: @body-foreground-color; + +//to provide common styles. +//please never pad manually, use multiples / divisons of @spacer instead. @border-radius: 2px; @spacer: 1em; @@ -40,7 +53,7 @@ } .box-shadow { - box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.15); } .header-styles { diff --git a/package.json b/package.json index a86aa18c8..f7d0a7153 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,10 @@ { "name": "bluegenes", - "description": "", + "version": "1.0.0", + "description": "Clojure-based GUI for InterMine", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, "main": "", "author": "Joshua Heimbach ", "license": "SEE LICENSE IN LICENCE", @@ -15,9 +19,8 @@ "dependencies": { "@cljs-oss/module-deps": "^1.1.1", "bootstrap": "^3.3.7", - "highlight.js": "^9.12.0", - "bootstrap-material-design": "https://github.com/intermine/bootstrap-material-design.git#im-0.0.2" + "bootstrap-material-design": "https://github.com/intermine/bootstrap-material-design.git#im-0.0.2", + "highlight.js": "^9.12.0" }, - "devDependencies": { - } + "devDependencies": {} } diff --git a/project.clj b/project.clj index 58e54c2a9..519b5cc15 100644 --- a/project.clj +++ b/project.clj @@ -1,10 +1,13 @@ -(def props {:version "0.9.4-SNAPSHOT"}) +(def props {:version "0.9.9-SNAPSHOT"}) (defproject org.intermine/bluegenes (:version props) + :licence "LGPL-2.1-only" + :description "Bluegenes is a Clojure-powered user interface for InterMine, the biological data warehouse" + :url "http://www.intermine.org" :dependencies [; Clojure [org.clojure/clojure "1.9.0"] [org.clojure/clojurescript "1.10.145"] - [org.clojure/core.async "0.3.443"] + [org.clojure/core.async "0.4.474"] ; MVC [re-frame "0.10.2"] @@ -33,6 +36,7 @@ [metosin/ring-http-response "0.9.0"] [ring-middleware-format "0.7.2"] + ; Dev tools [re-frisk "0.5.0"] @@ -114,13 +118,13 @@ :target-path "resources/public/css"} :profiles {:dev {:dependencies [[binaryage/devtools "0.9.7"]] - :resource-paths ["config/dev"] + :resource-paths ["config/dev" "tools" "config/defaults"] :plugins [[lein-figwheel "0.5.14"] [lein-doo "0.1.8"]]} :prod {:dependencies [] - :resource-paths ["config/prod"] + :resource-paths ["config/prod" "tools" "config/defaults"] :plugins []} - :uberjar {:resource-paths ["config/prod"] + :uberjar {:resource-paths ["config/prod" "config/defaults"] :prep-tasks ["clean" ["less" "once"] ["cljsbuild" "once" "min"] "compile"] :aot :all}} diff --git a/src/clj/bluegenes/core.clj b/src/clj/bluegenes/core.clj index 330ab815e..8a2e605d6 100644 --- a/src/clj/bluegenes/core.clj +++ b/src/clj/bluegenes/core.clj @@ -31,4 +31,4 @@ (catch Exception e (errorf "Unable to connect to database: %s" (.getMessage e)))) ; Start the Jetty server by passing in the URL routes defined in 'handler' (run-jetty handler {:port port :join? false}) - (infof "Bluegenes server started on port: %s" port))) + (infof "=== Bluegenes server started on port: %s" port))) diff --git a/src/clj/bluegenes/handler.clj b/src/clj/bluegenes/handler.clj index 7f8d5a984..426ed663d 100644 --- a/src/clj/bluegenes/handler.clj +++ b/src/clj/bluegenes/handler.clj @@ -1,6 +1,5 @@ (ns bluegenes.handler - (:require [compojure.core :refer [GET defroutes]] - [bluegenes.routes :as routes] + (:require [bluegenes.routes :as routes] [ring.util.response :refer [resource-response]] [ring.middleware.json :refer [wrap-json-response wrap-json-params]] [ring.middleware.params :refer [wrap-params]] @@ -16,11 +15,4 @@ ; Add session functionality to the Ring requests wrap-session ; Accept and parse request parameters in various formats - (wrap-restful-format :formats [:json :json-kw :transit-msgpack :transit-json]) - ; The rest are mostly replaced by wrap-restful-format but are being left for historical purposes: - ;wrap-params - ;wrap-restful-format - ;wrap-json-response - ;wrap-keyword-params - ;wrap-params -)) + (wrap-restful-format :formats [:json :json-kw :transit-msgpack :transit-json]))) diff --git a/src/clj/bluegenes/index.clj b/src/clj/bluegenes/index.clj index edd659ce3..03536b807 100644 --- a/src/clj/bluegenes/index.clj +++ b/src/clj/bluegenes/index.clj @@ -20,14 +20,14 @@ [:style "#wrappy{display:flex;justify-content:center;align-items:center;height:90vh;width:100%;flex-direction:column;font-family:sans-serif;font-size:2em;color:#999}#loader{flex-grow:1;display:flex;align-items:center;justify-content:center}.loader-organism{width:40px;height:0;display:block;border:12px solid #eee;border-radius:20px;opacity:.75;margin-right:-24px;animation-timing-function:ease-in;position:relative;animation-duration:2.8s;animation-name:pulse;animation-iteration-count:infinite}.worm{animation-delay:.2s}.zebra{animation-delay:.4s}.human{animation-delay:.6s}.yeast{animation-delay:.8s}.rat{animation-delay:1s}.mouse{animation-delay:1.2s}.fly{animation-delay:1.4s}@keyframes pulse{0%,100%{border-color:#3f51b5}15%{border-color:#9c27b0}30%{border-color:#e91e63}45%{border-color:#ff5722}60%{border-color:#ffc107}75%{border-color:#8bc34a}90%{border-color:#00bcd4}}\n "]) -; The of the landing page (defn head [] [:head loader-style [:title "InterMine 2.0 BlueGenes"] - ; CSS: - (include-css "https://cdnjs.cloudflare.com/ajax/libs/gridlex/2.2.0/gridlex.min.css") - (include-css "http://cdn.intermine.org/js/intermine/im-tables/2.0.0/main.sandboxed.css") + (include-css + "https://cdnjs.cloudflare.com/ajax/libs/gridlex/2.2.0/gridlex.min.css") + (include-css + "http://cdn.intermine.org/js/intermine/im-tables/2.0.0/main.sandboxed.css") (include-css "css/site.css") (include-css "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css") ; Meta data: diff --git a/src/clj/bluegenes/routes.clj b/src/clj/bluegenes/routes.clj index 20877d0f7..d6bb2b81a 100644 --- a/src/clj/bluegenes/routes.clj +++ b/src/clj/bluegenes/routes.clj @@ -1,9 +1,11 @@ (ns bluegenes.routes (:require [compojure.core :refer [GET defroutes context]] - [compojure.route :refer [resources]] + [compojure.route :refer [resources files]] [bluegenes.index :as index] [ring.util.response :refer [response]] [bluegenes.ws.auth :as auth] + [bluegenes.ws.tools :as tools] + [config.core :refer [env]] [bluegenes.ws.mymine :as mymine] [bluegenes.ws.ids :as ids])) @@ -12,11 +14,24 @@ (GET "/" req ; The user might have an active session. Pass their identity to the client to automatically ; log the user into the application: + (index/index (:identity (:session req)))) + + ;;serve compiled files, i.e. js, css, from the resources folder (resources "/") + + ;; serve all tool files in bluegenes/tools automatically. + ;; they can't go in the resource folder b/c then they get jarred + ;; when running uberjar or clojar targets, + ;; and make the jars about a million megabytes too big. + + (files "/tools" {:root (:bluegenes-tool-path env)}) + (GET "/version" [] (response {:version "0.1.0"})) + ; Anything within this route is the API web service: (context "/api" [] (context "/auth" [] auth/routes) + (context "/tools" [] tools/routes) (context "/mymine" [] mymine/routes) - (context "/ids" [] ids/routes))) \ No newline at end of file + (context "/ids" [] ids/routes))) diff --git a/src/clj/bluegenes/ws/tools.clj b/src/clj/bluegenes/ws/tools.clj new file mode 100644 index 000000000..62039a78b --- /dev/null +++ b/src/clj/bluegenes/ws/tools.clj @@ -0,0 +1,82 @@ +(ns bluegenes.ws.tools + (:require [compojure.core :refer [GET POST defroutes]] + [compojure.route :as route] + [clojure.string :refer [blank? join ends-with?]] + [cheshire.core :as cheshire] + [config.core :refer [env]] + [clojure.pprint :refer [pprint]] + [ring.util.http-response :as response] + [clojure.java.io :as io] + [taoensso.timbre :as timbre :refer [log warn]])) + +(def tool-path + "appends slash to the path if not present" + (if-let [the-tools (:bluegenes-tool-path env)] + (if (ends-with? the-tools "/") + the-tools + (str the-tools "/")) + (warn "No BlueGenes tool path found"))) + +(def tools-config (str tool-path "../package.json")) + +(defn get-tool-config + "check tool folder for config and other relevant files and return as + a map of useful info. This is used client-side by the browser to + load tools relevant for a given report page." + [tool path] + (let [tool-name (subs (str tool) 1) + debug (log :info "|---- tool:" tool) + path (join "/" [path tool-name]) + ;; this is the bluegenes-specific config. + bluegenes-config-path (str path "/config.json") + debuggg (log :info "|------ config path:" bluegenes-config-path) + config (cheshire/parse-string (slurp bluegenes-config-path) true) + ;;this is the default npm package file + package-path (str path "/package.json") + package (cheshire/parse-string (slurp package-path) true) + ;;optional preview image for each tool. + preview-image "/preview.png" + browser-path (str "/tools/" tool-name preview-image)] + ;; so many naming rules that conflict - we need three names. + ;; npm requires kebab-case bluegenes-tool-protvista + ;; but js vars forbid kebab case bluegenesToolProtvista + ;; humans want something with spaces "Protein viewer" + ;; this is terminally incompatible, hence three names. Argh. + {:names {:human (get-in config [:toolName :human]) + :cljs (get-in config [:toolName :cljs]) + :npm (get-in package [:name])} + :config config + :package package + ;; return image path if it exists, or false otherwise. + :hasimage (if (.exists (io/file (str path preview-image))) + browser-path + false)})) + +(defn installed-tools-list + "Return a list of the installed tools listed in the package.json file. " + [] + (try + (log :info "|-- looking for tool config file at: " tools-config) + (let [packages (cheshire/parse-string (slurp tools-config) true) + package-names (keys (:dependencies packages))] + package-names) + (catch Exception e (log :warn + (str "Couldn't find tools at " tools-config (.getMessage e) "- please run `npm init -y` in the tools directory and install your tools again."))))) + +(defn tools + "Format the list of tools as a REST response to our GET." + [session] + (log :info "Tools folder: ") + (log :info "|--" tool-path) + (let [installed-tools (installed-tools-list) + res + {:tools + (reduce + (fn [tool-list newitem] + (conj tool-list (get-tool-config newitem tool-path))) + #{} installed-tools)}] + (response/ok res))) + +(defroutes routes + ;;returns all available tools installed in the /tools folder + (GET "/all" session (tools session))) diff --git a/src/cljs/bluegenes/components/navbar/nav.cljs b/src/cljs/bluegenes/components/navbar/nav.cljs index e27d5ae22..d18c79fab 100644 --- a/src/cljs/bluegenes/components/navbar/nav.cljs +++ b/src/cljs/bluegenes/components/navbar/nav.cljs @@ -86,7 +86,7 @@ (if (= :default id) (clojure.string/join " - " [(:name details) "Default"]) (:name details))]]) @(subscribe [:mines]))) - [:li.special [:a {:on-click #(navigate! "/debug")} ">_ Developer"]])]))) + [:li.special [:a {:on-click #(navigate! "/debug/main")} ">_ Developer"]])]))) (defn logged-in [] (let [identity (subscribe [:bluegenes.subs.auth/identity])] diff --git a/src/cljs/bluegenes/developer.cljs b/src/cljs/bluegenes/developer.cljs deleted file mode 100644 index 7ebeab966..000000000 --- a/src/cljs/bluegenes/developer.cljs +++ /dev/null @@ -1,83 +0,0 @@ -(ns bluegenes.developer - (:require [re-frame.core :as re-frame :refer [subscribe dispatch]] - [json-html.core :as json-html] - [bluegenes.components.icons :as icons] - [bluegenes.persistence :as persistence] - [accountant.core :refer [navigate!]])) - -(defn mine-config [] - (let [current-mine (subscribe [:current-mine]) - mines (subscribe [:mines]) - url (str "http://" (:root (:service @current-mine)))] - (fn [] - [:div.panel.container [:h3 "Current mine: "] - [:p (:name @current-mine) " at " - [:a {:href url} url]] - [:form - [:legend "Select a new mine to draw data from:"] - (into [:div.form-group.mine-choice - {:on-change (fn [e] - (dispatch [:set-active-mine (keyword (aget e "target" "value"))])) - :value "select-one"}] - (map (fn [[id details]] - [:label - {:class (cond (= id (:id @current-mine)) "checked")} - [:input - {:type "radio" - :name "urlradios" - :id id - :defaultChecked (= id (:id @current-mine)) - :value id}] (:common details)]) @(subscribe [:mines]))) - [:button.btn.btn-primary.btn-raised - {:on-click (fn [e] (.preventDefault e))} "Save"]]]))) - -(defn version-number [] - [:div.panel.container - [:h3 "Client Version: "] - [:code (str bluegenes.core/version)]]) - -(defn localstorage-destroyer [] - (fn [] - [:div.panel.container [:h3 "Delete local storage: "] - [:form - [:p "This will delete the local storage settings included preferred intermine instance, model, lists, and summaryfields. Model, lists, summaryfields should be loaded afresh every time anyway, but here's the easy pressable button to be REALLY SURE: "] - [:button.btn.btn-primary.btn-raised - {:on-click - (fn [e] - (.preventDefault e) - (persistence/destroy!) - (.reload js/document.location true))} "Delete bluegenes localstorage... for now."]]])) - -(defn iconview [] - [:div.panel.container [:h3 "All icons defs in the icons file (components/icons.cljs.)"] - [:div.icon-size "Bonus classes for easy sizing: " - [:div.icon-sizing-example - [:div.demo - "default" - [:svg.icon.icon-intermine [:use {:xlinkHref "#icon-intermine"}]]] - [:div.demo - [:code ".icon-2x"] - [:svg.icon.icon-2x.icon-intermine [:use {:xlinkHref "#icon-intermine"}]]] - [:div.demo - [:code ".icon-3x"] - [:svg.icon.icon-3x.icon-intermine [:use {:xlinkHref "#icon-intermine"}]]] - [:div.demo - [:code ".icon-4x"] - [:svg.icon.icon-4x.icon-intermine [:use {:xlinkHref "#icon-intermine"}]]]] - [:div "example:" [:code "[:svg.icon-3x.icon-intermine [:use {:xlinkHref \"#icon-intermine\"}]]"]]] - (let [icon-names (rest (last (icons/icons)))] - [:table.icon-view [:tbody - (map (fn [[icon-symbol]] - (let [icon-name (last (clojure.string/split icon-symbol "#"))] - [:tr {:key icon-name} - [:td [:svg.icon {:class icon-name} [:use {:xlinkHref (str "#" icon-name)}]]] - [:td icon-name] - [:td [:div.code "[:svg.icon." icon-name " [:use {:xlinkHref \"#" icon-name "\"}]]"]]])) icon-names)]])]) - -(defn debug-panel [] - (fn [] - [:div.developer - [mine-config] - [localstorage-destroyer] - [version-number] - [iconview]])) diff --git a/src/cljs/bluegenes/events/boot.cljs b/src/cljs/bluegenes/events/boot.cljs index dffdadc32..a5a2869c8 100644 --- a/src/cljs/bluegenes/events/boot.cljs +++ b/src/cljs/bluegenes/events/boot.cljs @@ -242,7 +242,7 @@ (fn [db [_ mine-kw model]] (-> db (assoc-in [:mines mine-kw :service :model] model) - (assoc-in [:mines mine-kw :default-object-types] (preferred-fields model))))) + (assoc-in [:mines mine-kw :default-object-types] (sort (preferred-fields model)))))) (reg-event-fx :assets/fetch-model diff --git a/src/cljs/bluegenes/pages/developer/devhome.cljs b/src/cljs/bluegenes/pages/developer/devhome.cljs new file mode 100644 index 000000000..c402bf408 --- /dev/null +++ b/src/cljs/bluegenes/pages/developer/devhome.cljs @@ -0,0 +1,81 @@ +(ns bluegenes.pages.developer.devhome + (:require [re-frame.core :as re-frame :refer [subscribe dispatch]] + [bluegenes.pages.developer.events :as events] + [bluegenes.pages.developer.subs :as subs] + [bluegenes.pages.developer.icons :as icons] + [bluegenes.pages.developer.tools :as tools] + [bluegenes.persistence :as persistence] + [clojure.string :refer [blank?]] + [imcljs.internal.utils :as utils :refer [missing-http?-]] + [accountant.core :refer [navigate!]])) + +(defn nav [] + [:ul.dev-navigation + [:li [:a {:on-click #(navigate! "/debug/main")} + [:svg.icon.icon-cog [:use {:xlinkHref "#icon-cog"}]] "Debug Console"]] + [:li + [:a {:on-click #(navigate! "/debug/tool-store")} + [:svg.icon.icon-star-full [:use {:xlinkHref "#icon-star-full"}]] "Tool 'App Store'"]] + [:li [:a {:on-click #(navigate! "/debug/icons")} + [:svg.icon.icon-intermine [:use {:xlinkHref "#icon-intermine"}]] "Icons"]]]) + +(defn mine-config [] + (let [current-mine (subscribe [:current-mine]) + mines (subscribe [:mines]) + minelink (:root (:service @current-mine)) + url (if missing-http?- (str "http://" minelink) minelink)] + (fn [] + [:div.panel.container [:h3 "Current mine: "] + [:p (:name @current-mine) " at " + [:a {:href url} url]] + [:form + [:legend "Select a new mine to draw data from:"] + (into [:div.form-group.mine-choice + {:on-change (fn [e] + (dispatch [:set-active-mine (keyword (aget e "target" "value"))])) + :value "select-one"}] + (map (fn [[id details]] + (let [mine-name (if (blank? (:name details)) id (:name details))] + [:label + {:class (cond (= id (:id @current-mine)) "checked")} + [:input + {:type "radio" + :name "urlradios" + :id id + :defaultChecked (= id (:id @current-mine)) + :value id}] mine-name])) @(subscribe [:mines]))) [:button.btn.btn-primary.btn-raised + {:on-click (fn [e] (.preventDefault e))} "Save"]]]))) + +(defn version-number [] + [:div.panel.container + [:h3 "Client Version: "] + [:code (str bluegenes.core/version)]]) + +(defn localstorage-destroyer [] + (fn [] + [:div.panel.container [:h3 "Delete local storage: "] + [:form + [:p "This will delete the local storage settings included preferred intermine instance, model, lists, and summaryfields. Model, lists, summaryfields should be loaded afresh every time anyway, but here's the easy pressable button to be REALLY SURE: "] + [:button.btn.btn-primary.btn-raised + {:on-click + (fn [e] + (.preventDefault e) + (persistence/destroy!) + (.reload js/document.location true))} "Delete bluegenes localstorage... for now."]]])) + +(defn debug-panel [] + (fn [] + (let [panel (subscribe [::subs/panel])] + [:div.developer + [nav] + (cond + (= @panel "main") + [:div + [:h1 "Debug console"] + [mine-config] + [localstorage-destroyer] + [version-number]] + (= @panel "tool-store") + [tools/tool-store] + (= @panel "icons") + [icons/iconview])]))) diff --git a/src/cljs/bluegenes/pages/developer/events.cljs b/src/cljs/bluegenes/pages/developer/events.cljs new file mode 100644 index 000000000..1b9f6f2a1 --- /dev/null +++ b/src/cljs/bluegenes/pages/developer/events.cljs @@ -0,0 +1,26 @@ +(ns bluegenes.pages.developer.events + (:require-macros [cljs.core.async.macros :refer [go go-loop]]) + (:require [re-frame.core :refer [subscribe reg-event-db reg-event-db reg-event-fx]] + [bluegenes.effects :as fx] + [cljs.core.async :refer [js @tools)) + (into + [:div.tool-list] + (map + (fn [tool] + [:div.tool + [:h2 (get-in tool [:names :human])] + (if (:hasimage tool) + [:div.tool-preview [:img {:src (:hasimage tool) :height "220px"}]] + [:div.tool-no-preview "No tool preview available"]) + [:div.details + [:div.description [:svg.icon.icon-info [:use {:xlinkHref "#icon-info"}]] (get-in tool [:package :description])] + [output-tool-classes (get-in tool [:config :classes])]]]) + (:tools @tools))))) + +(defn tool-store + "Page structure for tool store UI" + [] + [:div.tool-store + [:h1 "Tool Store"] + [:div + [:div.info + [:svg.icon.icon-info [:use {:xlinkHref "#icon-info"}]] + [:p "Showing all tools that are currently installed for Report Pages. To add more tools, see the " + [:a {:href "https://github.com/intermine/bluegenes/tree/dev/docs"} "BlueGenes Documentation"] "."]] + [tool-list]]]) diff --git a/src/cljs/bluegenes/pages/mymine/views/browser.cljs b/src/cljs/bluegenes/pages/mymine/views/browser.cljs index 5f46d387c..c35ad0cce 100644 --- a/src/cljs/bluegenes/pages/mymine/views/browser.cljs +++ b/src/cljs/bluegenes/pages/mymine/views/browser.cljs @@ -1,4 +1,4 @@ -(ns bluegenes.sections.mymine.views.browser +(ns bluegenes.pages.mymine.views.browser (:require [re-frame.core :refer [subscribe dispatch]] [reagent.core :as r] [oops.core :refer [oget ocall]] diff --git a/src/cljs/bluegenes/pages/querybuilder/views.cljs b/src/cljs/bluegenes/pages/querybuilder/views.cljs index e3e10e555..3351fff65 100644 --- a/src/cljs/bluegenes/pages/querybuilder/views.cljs +++ b/src/cljs/bluegenes/pages/querybuilder/views.cljs @@ -179,7 +179,7 @@ :lists @lists :code (:code con) :on-remove (fn [] (dispatch [:qb/enhance-query-remove-constraint path idx])) - ;:possible-values (when (some? (:possible-values properties)) (map :item (:possible-values properties))) + :possible-values (when (some? (:possible-values properties)) (map :item (:possible-values properties))) :typeahead? true :value (or (:value con) (:values con)) :op (:op con) diff --git a/src/cljs/bluegenes/pages/reportpage/components/tools.cljs b/src/cljs/bluegenes/pages/reportpage/components/tools.cljs new file mode 100644 index 000000000..48fc1d046 --- /dev/null +++ b/src/cljs/bluegenes/pages/reportpage/components/tools.cljs @@ -0,0 +1,83 @@ +(ns bluegenes.pages.reportpage.components.tools + (:require [re-frame.core :refer [subscribe dispatch]] + [accountant.core :refer [navigate!]] + [oops.core :refer [ocall+ oapply oget oget+ oset!]] + [bluegenes.pages.reportpage.subs :as subs])) + +;;fixes the inability to iterate over html vector-like things +(extend-type js/HTMLCollection + ISeqable + (-seq [array] (array-seq array 0))) + +(defn create-package + "Format the arguments for a tool api compliant tool, such that a tool knows what to display" + [] + (let [package-details (subscribe [:panel-params]) + package {:class (:type @package-details) + ;;we'll need to reformat deep links if we want this to not be hardcoded. + :format "id" + :value (:id @package-details)}] + (clj->js package))) + +(defn run-script + "Executes a tool-api compliant main method to initialise a tool" + [tool tool-id] + ;;the default method signature is + ;;package-name(el, service, package, state, config) + (let [el (.getElementById js/document tool-id) + service (clj->js (:service @(subscribe [:current-mine]))) + package (create-package) + config (clj->js (:config tool))] + (ocall+ js/window (str (get-in tool [:names :cljs]) ".main") el service package nil config))) + +(defn fetch-script + ;; inspired by https://stackoverflow.com/a/31374433/1542891 + ;; I don't much like fetching the script in the view, but given + ;; that this is heavy dom manipulation it seems necessary. + ;; TODO: could active scripts in appdb be dereferenced and added to + ;; the head automatically? That might be better if possible. + "Dynamically inserts the tool api script into the head of the document" + [tool tool-id] + (let [script-tag (.createElement js/document "script") + head (first (.getElementsByTagName js/document "head")) + tool-path (get-in tool [:config :files :js])] + (if tool-path + (do + ;;fetch script from npm's node_modules directory + (oset! script-tag "src" (str "/tools/" (get-in tool [:names :npm]) "/" tool-path)) + ;;run-script will automatically be triggered when the script loads + (oset! script-tag "onload" #(run-script tool tool-id)) + ;;append script to dom + (.appendChild head script-tag)) + ;; there must be a script tag. If there isn't, console error. + (.error js/console "%cNo script path provided for %s" "background:#ccc;border-bottom:solid 3px indianred; border-radius:2px;" (get-in tool [:names :human]))))) + +(defn fetch-styles + "If the tool api script has a stylesheet as well, load it and insert into the doc" + [tool] + (let [style-tag (.createElement js/document "link") + head (first (.getElementsByTagName js/document "head")) + style-path (get-in tool [:config :files :css])] + (cond style-path + ;;fetch stylesheet and set some properties + (do (oset! style-tag "href" (str "/tools/" (get-in tool [:names :npm]) "/" style-path)) + (oset! style-tag "type" "text/css") + (oset! style-tag "rel" "stylesheet") + ;;append to dom + (.appendChild head style-tag))))) + +(defn main + "Initialise all the tools on the page" + [] + (let [toolses (subscribe [::subs/tools-by-current-type])] + (.log js/console "%c@toolses" "color:mediumorchid;font-weight:bold;" (clj->js @toolses)) + (into [:div.tools] + (map + (fn [tool] + (let [tool-id (gensym (get-in tool [:config :toolName :cljs]))] + (fetch-script tool tool-id) + (fetch-styles tool) + [:div.tool {:class (get-in tool [:names :cljs])} + [:h3 (get-in tool [:names :human])] + [:div {:id tool-id}]])) + @toolses)))) diff --git a/src/cljs/bluegenes/pages/reportpage/events.cljs b/src/cljs/bluegenes/pages/reportpage/events.cljs index b7e1440a4..47a9c21c5 100644 --- a/src/cljs/bluegenes/pages/reportpage/events.cljs +++ b/src/cljs/bluegenes/pages/reportpage/events.cljs @@ -2,6 +2,7 @@ (:require-macros [cljs.core.async.macros :refer [go go-loop]]) (:require [re-frame.core :as re-frame :refer [reg-event-db reg-event-fx reg-fx dispatch subscribe]] [bluegenes.db :as db] + [bluegenes.effects :as fx] [cljs.core.async :refer [put! chan ! timeout close!]] [imcljs.fetch :as fetch] [imcljs.path :as path])) @@ -32,4 +33,25 @@ {:db (-> db (assoc :fetching-report? true) (dissoc :report)) - :dispatch [:fetch-report (keyword mine) type id]})) \ No newline at end of file + :dispatch-n [[::fetch-tools nil] + [:fetch-report (keyword mine) type id]]})) + +(reg-event-fx + ::fetch-tools + (fn [{db :db} [x tool-type]] + {:db db + ::fx/http {:method :get + :on-success [::store-tools] + :uri (str "/api/tools/all")}})) + +(defn aggregate-classes [m tool] + ;;Oh my, I had help on this one. https://stackoverflow.com/questions/48010316/clojure-clojurescript-group-by-a-map-on-multiple-values/48010630#48010630 + (->> (get-in tool [:config :classes]) + (reduce (fn [acc elem] + (update acc elem conj tool)) + m))) (reg-event-db + ::store-tools + (fn [db [_ tools]] + (-> + (assoc-in db [:tools :all] (:tools tools)) + (assoc-in [:tools :classes] (reduce aggregate-classes {} (:tools tools)))))) diff --git a/src/cljs/bluegenes/pages/reportpage/subs.cljs b/src/cljs/bluegenes/pages/reportpage/subs.cljs index b6c5ebc3a..2dd4503dd 100644 --- a/src/cljs/bluegenes/pages/reportpage/subs.cljs +++ b/src/cljs/bluegenes/pages/reportpage/subs.cljs @@ -1,6 +1,18 @@ (ns bluegenes.pages.reportpage.subs (:require [re-frame.core :refer [reg-sub]] - [imcljs.path :as im-path])) + [imcljs.path :as im-path] + [clojure.walk :refer [postwalk postwalk-demo]])) + +(reg-sub + ::tools-by-current-type + (fn [db [_]] + (let [tool-type (get-in db [:panel-params :type])] + (get-in db [:tools :classes tool-type])))) + +(reg-sub + ::all-tools + (fn [db] + (get-in db [:tools :all]))) (reg-sub ::a-table (fn [db [_ location]] @@ -68,8 +80,21 @@ ; And return just the vals (map - - last))))) +(reg-sub ::current-templates + :<- [::current-mine] + :<- [::templates] + (fn [[current-mine current-templates]] + (get current-templates current-mine))) +(reg-sub ::non-empty-collections-and-references + :<- [:current-model] + :<- [:panel-params] + :<- [:report] + (fn [[model params]] + (let [collections (vals (get-in model [:classes (keyword (:type params)) :collections])) + references (vals (get-in model [:classes (keyword (:type params)) :references])) + non-empty-collections (filter (fn [c] (> (get-in model [:classes (keyword (:referencedType c)) :count]) 0)) collections) + non-empty-references (filter (fn [c] (> (get-in model [:classes (keyword (:referencedType c)) :count]) 0)) references)] + (concat non-empty-references non-empty-collections)))) diff --git a/src/cljs/bluegenes/pages/reportpage/views.cljs b/src/cljs/bluegenes/pages/reportpage/views.cljs index e7e92b5bb..ecaf0ec22 100644 --- a/src/cljs/bluegenes/pages/reportpage/views.cljs +++ b/src/cljs/bluegenes/pages/reportpage/views.cljs @@ -8,6 +8,7 @@ [bluegenes.pages.reportpage.components.minelinks :as minelinks] [accountant.core :refer [navigate!]] [bluegenes.pages.reportpage.subs :as subs] + [bluegenes.pages.reportpage.components.tools :as tools] [im-tables.views.core :as im-table] [imcljs.path :as im-path])) @@ -32,6 +33,47 @@ (def clj-name name) +(defn collections-and-references [service current-mine-name object-type id] + (let [summary-fields (subscribe [:current-summary-fields]) + nonempty-collections-references (subscribe [::subs/non-empty-collections-and-references])] + (into [:div] + (map (fn [{:keys [name referencedType displayName] :as x}] + ^{:key (str id (:name x))} + [tbl {:loc [:report-page id (:name x)] + :service (:service @service) + :title displayName + :query {:from object-type + :select (map (partial str object-type "." (:name x) ".") (map strip-class (get @summary-fields (keyword referencedType)))) + :where [{:path (str object-type ".id") + :op "=" + :value id}]} + :settings {:pagination {:limit 5} + :links {:vocab {:mine (clj-name (or @current-mine-name ""))} + :url (fn [vocab] + (str "#/reportpage/" + (:mine vocab) "/" + (:class vocab) "/" + (:objectId vocab)))}}}]) + @nonempty-collections-references)))) + +(defn templates-for-entity [service current-mine-name id] + (let [runnable-templates (subscribe [::subs/runnable-templates])] + (into [:div] + (map (fn [{:keys [name title] :as t}] + (let [key nil] + [tbl {:loc [:report-page id (:name t)] + :service (:service @service) + :title title + :query t + :settings {:pagination {:limit 5} + :links {:vocab {:mine (clj-name (or @current-mine-name ""))} + :url (fn [vocab] + (str "#/reportpage/" + (:mine vocab) "/" + (:class vocab) "/" + (:objectId vocab)))}}}])) + @runnable-templates)))) + (defn main [] (let [params (subscribe [:panel-params]) report (subscribe [:report]) @@ -40,9 +82,7 @@ service (subscribe [:current-mine]) model (subscribe [:current-model]) current-mine-name (subscribe [:current-mine-name]) - fetching-report? (subscribe [:fetching-report?]) - summary-fields (subscribe [:current-summary-fields]) - runnable-templates (subscribe [::subs/runnable-templates])] + fetching-report? (subscribe [:fetching-report?])] (fn [] [:div.container.report (let [; TODO Move the following heavy lifting to the events and subs: @@ -50,7 +90,6 @@ references (vals (get-in @model [:classes (keyword (:type @params)) :references])) non-empty-collections (filter (fn [c] (> (get-in @model [:classes (keyword (:referencedType c)) :count]) 0)) collections) non-empty-references (filter (fn [c] (> (get-in @model [:classes (keyword (:referencedType c)) :count]) 0)) references)] - (let [{:keys [type id]} @params] [:div ; Only show the body of the report when the summary has loaded @@ -61,35 +100,6 @@ (when (= "Gene" (:type @params)) [minelinks/main (:id @params)]) (when (:summary @report) [:div.report-body - (into [:div] - (map (fn [{:keys [name title] :as t}] - (let [key nil] - [tbl {:loc [:report-page id name] - :service (:service @service) - :title title - :query t - :settings {:pagination {:limit 5} - :links {:vocab {:mine (clj-name (or @current-mine-name ""))} - :url (fn [vocab] (str "#/reportpage/" - (:mine vocab) "/" - (:class vocab) "/" - (:objectId vocab)))}}}])) - @runnable-templates)) - (into [:div] - (map (fn [{:keys [name referencedType displayName] :as x}] - (let [key (str id name)] - ^{:key key} [tbl {:loc [:report-page id name] - :service (:service @service) - :title displayName - :query {:from type - :select (map (partial str type "." name ".") (map strip-class (get @summary-fields (keyword referencedType)))) - :where [{:path (str type ".id") - :op "=" - :value id}]} - :settings {:pagination {:limit 5} - :links {:vocab {:mine (clj-name (or @current-mine-name ""))} - :url (fn [vocab] (str "#/reportpage/" - (:mine vocab) "/" - (:class vocab) "/" - (:objectId vocab)))}}}])) - (concat non-empty-references non-empty-collections)))])])]))]))) + [tools/main] + [collections-and-references service current-mine-name type id] + [templates-for-entity service current-mine-name id]])])]))]))) diff --git a/src/cljs/bluegenes/pages/results/enrichment/events.cljs b/src/cljs/bluegenes/pages/results/enrichment/events.cljs index f16e71a50..3d1fc3d6e 100644 --- a/src/cljs/bluegenes/pages/results/enrichment/events.cljs +++ b/src/cljs/bluegenes/pages/results/enrichment/events.cljs @@ -13,7 +13,7 @@ [ajax.core :as ajax] [bluegenes.interceptors :refer [clear-tooltips]] [dommy.core :refer-macros [sel sel1]] - ;[bluegenes.sections.saveddata.events] + ;[bluegenes.pages.saveddata.events] [accountant.core :as accountant] [bluegenes.specs :as specs] [bluegenes.interceptors :refer [abort-spec]])) diff --git a/src/cljs/bluegenes/routes.cljs b/src/cljs/bluegenes/routes.cljs index 4ba39b818..66690f224 100644 --- a/src/cljs/bluegenes/routes.cljs +++ b/src/cljs/bluegenes/routes.cljs @@ -20,8 +20,10 @@ (defroute "/" [] (dispatch [:set-active-panel :home-panel])) - (defroute "/debug" [] - (dispatch [:set-active-panel :debug-panel])) + (defroute "/debug/:panel" [panel] + (dispatch [:set-active-panel :debug-panel + nil + [:bluegenes.pages.developer.events/panel panel]])) (defroute "/help" [] (dispatch [:set-active-panel :help-panel])) @@ -87,4 +89,4 @@ (js/ga "send" "pageview" (secretary/locate-route-value path))) (catch :default e (.error js/console "Unable to dispatch google analytics for this page: " path " make sure the url is formed correctly. Stacktrace: " e))) (secretary/dispatch! path)) - :path-exists? (fn [path] (secretary/locate-route path))})) \ No newline at end of file + :path-exists? (fn [path] (secretary/locate-route path))})) diff --git a/src/cljs/bluegenes/views.cljs b/src/cljs/bluegenes/views.cljs index 28d4a6be1..831106a91 100644 --- a/src/cljs/bluegenes/views.cljs +++ b/src/cljs/bluegenes/views.cljs @@ -1,7 +1,7 @@ (ns bluegenes.views (:require [re-frame.core :as re-frame :refer [subscribe dispatch]] [json-html.core :as json-html] - [bluegenes.developer :as dev] + [bluegenes.pages.developer.devhome :as dev] [bluegenes.components.navbar.nav :as nav] [bluegenes.components.icons :as icons] [bluegenes.pages.home.views :as home] @@ -46,7 +46,7 @@ (defmulti panels identity) (defmethod panels :home-panel [] [home/main]) -(defmethod panels :debug-panel [] [dev/debug-panel]) +(defmethod panels :debug-panel [panel] [dev/debug-panel]) (defmethod panels :templates-panel [] [templates/main]) (defmethod panels :reportpage-panel [] [reportpage/main]) (defmethod panels :upload-panel [] [idresolver/main]) diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 000000000..72e9a507e --- /dev/null +++ b/tools/README.md @@ -0,0 +1,15 @@ +## Tools folder + +This folder is only for tool-api compliant tools. + +Any file/folder matching the pattern bluegenes** is automatically ignored by git. + +To install a tool, run `npm install NameOfYourTool`. This assumes you have a +recent version of [npm and node](https://nodejs.org/en/download/) installed (you can check by running `node --version`). A full list of tools is on npm under the tag +[bluegenes-intermine-tool](https://www.npmjs.com/search?q=keywords:bluegenes-intermine-tool). + +## For more details about writing tools, see: + +- [tool admin/installation guide](docs/tools.md) +- [tutoral: how to make a new tool](docs/tool-api-tutorial.md) +- [tool api specs](docs/tool-api-tutorial.md) diff --git a/tools/docs/img/console-log-preview-tutorial.png b/tools/docs/img/console-log-preview-tutorial.png new file mode 100644 index 000000000..daf69995d Binary files /dev/null and b/tools/docs/img/console-log-preview-tutorial.png differ diff --git a/tools/docs/tool-api-tutorial.md b/tools/docs/tool-api-tutorial.md new file mode 100644 index 000000000..b527f0b24 --- /dev/null +++ b/tools/docs/tool-api-tutorial.md @@ -0,0 +1,314 @@ +# Bluegenes Tool API Tutorial + +You don't need to know Clojure/Clojurescript in order to create a data vis / analysis tool for BlueGenes. This document will walk you through converting your favourite javascript tool into a BlueGenes compatible tool. You may wish to keep the [Tool API documentation](tool-api.md) open in another tab. + +For this tutorial, we'll be creating a tool that fetches GO terms related to a single gene, broken down by namespace. Using the query builder and im-tables, we can generate the imjs query required, and we'll save it for later: + +```javascript +var query = { + "from": "Gene", + "select": [ + "goAnnotation.ontologyTerm.name", + "goAnnotation.evidence.code.code", + "goAnnotation.evidence.code.name", + "goAnnotation.ontologyTerm.namespace" + ], + "where": [ + { + "path": "symbol", + "op": "=", + "value": "zen" + } + ] +}; +``` + +## Getting ready: + +What you'll need to build the tool: + +- npm and node 7+, preferably installed via [nvm](https://github.com/creationix/nvm) +- A JavaScript tool that you would like to implement in a BlueGenes report page. See our [Tool Board](https://github.com/intermine/bluegenes/projects/2#column-943519) for some options. +- [yeoman](http://yeoman.io/) (You can install via `npm install -g yo`). +- A text editor and modern browser. + +Optionally, to test your tool: + +- [A local BlueGenes install](https://github.com/intermine/bluegenes/blob/dev/docs/getting-started.md) + +We'll expect a basic familiarity with programming - specifically [JavaScript](https://developer.mozilla.org/en-US/docs/Glossary/JavaScript), [LESS](http://lesscss.org/)/[CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), and to a lesser degree the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction). + +## Generate your tool scaffold + +If you look at the [Tool API documentation](tool-api.md), you many note that BlueGenes tools have a specific folder structure. The good news is that you don't have to create all these files yourself! We have a yeoman generator which makes things easier for you. + +### Install the Yeoman Generator + +Assuming you've installed yeoman already, run the following command to install the BlueGenes tool generator: + +```bash + npm install -g @intermine/generator-bluegenes-tool +``` + +Brilliant! You'll only need to install the generator once. + +### Generate a new project: The wizard + +In the parent folder where you'd like your project to live, we need to make a new folder with your preferred name (perhaps something like `~/projects/myBluegenesTool`), and then generate a new tool scaffold. + +``` +mkdir myBluegenesTool +cd myBluegenesTool +yo @intermine/bluegenes-tool +``` + +This will walk you through a few questions in a step-by-step wizard. We'll walk through what each one means now: + +1. **? What shall we name your project? This is a computer name with no spaces or special characters.** a name for your tool. We'd recommend prefixing every tool with the word bluegenesTool, e.g. `bluegenesToolProtVista` or `bluegenesToolCytoscape`. If you're not sure what you want to call your project yet, you could name it `bluegenesToolGOTerms`. +2. **NPM package name? This is a computer name with no capital letters or special characters apart from the - dash** - something like `bluegenes-tool-go-terms` would be perfect. +2. **Thanks! Now, give me a human name for the project - e.g. "Protein Feature Viewer"** - a nice friendly name for humans to read - spaces are allowed. Let's call ours "GO Terms" +3. **Fabulous. Which report pages do you expect this tool to work for, e.g. "Gene" or "Protein"? Separate with commas and put * for all.** This needs to be an InterMine class or classes. Since GO stands for Gene Ontology, let's enter `Gene`, to show this tool on all gene report pages. +4. **Awesome. What type of InterMine data can you work with?** - Right now, you should only select `id` - the tool API as of version 1 only supports the report page. Selecting id means that the tool will be passed the id of a single InterMine entity - e.g. a protein might be represented by the ID 4815162342. +5. **What's your name?** Hopefully you know the answer to this one! ;) This is important for package.json, which we will automatically generate you. +6. **Your email** As above - it's useful for package.json. +7. **Your website** As above - it's useful for package.json. +8. **Which license do you want to use?** we're pre-provided a few licences to choose from. Whichever you choose, remember that InterMine is LGPL 2.1, meaning it can be taken into private repositories. This is _not_ compatible with viral licences like GPL. For your tool, LGPL, MIT, or Apache might be good choices that are compatible with LGPL. + +Once you select a licence, the yeoman installer should set up your repository and all the files within based on the responses you gave to the wizard. This may take a minute or two. + +### Setting up your newly scaffolded tool + +Take a few minutes to look through your newly generated tool files. We'll be doing most of our work in the `src/index.js` file, and previewing it via demo.html. + +#### fetching and formatting our data + +Open up src/index.js and take a quick look. There's a lot of dummy / example code that we can delete, but make sure not to delete the main method, `export function main (el, service, imEntity, state, config)`. Instead, the code we write should be INSIDE this method (or called from inside it). This is the method that BlueGenes will automatically call when it loads your tool. + +You can read more about the arguments for this method in the [tool API docs](tool-api.md). In this tutorial, we'll use these args: + +- `service` provides the URL and token for communicating with a mine, +- `imEntity` provides the details of the report page we're looking at - i.e. it might be that the tool is being passed the objectid of a gene, or the accession of a protein... +- `el` - the id of the html element we can attach any text / visualisations to. + +Let's write some code. Remember the query we looked up, earlier? We want to use +imjs to load the query results and then display them on the page. Let's copy the + query into index.js and print the results to the console to see if it works like we hoped. + + Here's my first pass at index.js, deleting all the boilerplate and logging my query to the console: + +```javascript +//make sure to export main, with the signature +export function main (el, service, imEntity, state, config) { + + var query = { + "from": imEntity.class, //this should always be Gene, because we configured this tool to display data on Gene report pages. + "select": [ + "goAnnotation.ontologyTerm.name", + "goAnnotation.evidence.code.code", + "goAnnotation.evidence.code.name", + "goAnnotation.ontologyTerm.namespace" + ], + "where": [ + { + "path": imEntity.format, //this translates to id, based on our tool config. + "op": "=", + "value": imEntity.value // BlueGenes will pass this dynamically. + } + ] + }; + //fetch data using imjs, which is available on the window. + var goTerms = new imjs.Service(service) + .records(query) + .then(function(response) { + //we'll add more code here, but in the meantime, print the result to the console. + console.log(response); + }); +} +``` + +Note the use of `imEntity` to form the query. You might be wondering where this data is coming from. +When you're using BlueGenes, it'll be passed automatically from BlueGenes to your tool. Since we're just working in standalone mode at the minute, hop over to demo.html instead to see what's being passed over as arguments. You can tweak the `dataToInitialiseToolWith` variable to make sure it reflects sample data you want to work with. You can also change the mine URL if needed! + +Set your `dataToInitialiseToolWith` variable in demo.html to look like this. You may need to tweak the id depending on which InterMine you are using. (To get an id in a JSP-based InterMine, go to a report page and grab it from the URL. The url in the code snippet below came from this report page: [http://www.humanmine.org/humanmine/report.do?id=1276963](http://www.humanmine.org/humanmine/report.do?id=1276963)) + +```javascript +dataToInitialiseToolWith = { + "class": "Gene", + "format": "id", + // right now this id corresponds to Human GATA1 + // this is a bit fragile since IDs change with every build + // but we don't want to make the code more complicated than + // needed for the demo. + "value": 1276963 +}, +``` + +Okay, now that we have our sample data set up and some code, let's test if it works! + +In your console, you'll need to bundle your js file. To do so, run `npm run build`. You should notice that we now have a folder called `dist` which contains a file: `bundle.js`. How can we inspect to see if it is working or not? Well, we've included a handy little server for just this purpose! To test it out, run `npm run dev` and then navigate to +[http://localhost:3456](http://localhost:3456). If everything went well, you should see a white screen, and your query results logged to the [javascript console](https://www.digitalocean.com/community/tutorials/how-to-use-the-javascript-developer-console). Great! It should look roughly like this: + +![output of console log, showing an array of GO terms associated with the Gene we searched for.](img/console-log-preview-tutorial.png) + +##### Making something display on the screen... + +White screens are boring. Let's output our results by iterating through them, sorting by namespace, and printing to the screen. In the demo below, we've used a quick and easy technique of building up the HTML using strings; we wouldn't recommend this for any large applications but for a small demo tool it's sufficient - a framework would definitely be overkill! + +Here's the same code as before, but expanded to output the results visually in the HTML. In particular, note how we've set `el.innerHTML = ` - this is how we get our work to display in BlueGenes. + +```javascript +//make sure to export main, with the signature +export function main(el, service, imEntity, state, config) { + + //this query fetches GO terms and evidence codes associated with the given gene. + var query = { + "from": imEntity.class, //In this case, this should always be Gene, because + // we configured this tool to display data on Gene report pages. It's still + // better practice to use imEntity.class rather than hardcoding to gene - + // consider a tool that worked for genes OR proteins, for example! + "select": [ + "goAnnotation.ontologyTerm.name", + "goAnnotation.evidence.code.code", + "goAnnotation.evidence.code.name", + "goAnnotation.ontologyTerm.namespace" + ], + "orderBy": [{ + "path": "goAnnotation.ontologyTerm.namespace", + "direction": "ASC" + }], + "where": [{ + "path": imEntity.format, //this translates to id, based on our tool config. + "op": "=", + "value": imEntity.value // BlueGenes will pass this dynamically. + }] + }; + + //fetch data using imjs, which is available on the window. + var goTerms = new imjs.Service(service) + .records(query) + .then(function(response) { + //process results so they're grouped by their namespaces + var terms = resultsToNamespaceBuckets(response); + // output the results into HTML and add to the element provided + // in the head of the main method. + el.innerHTML = buildVisualOutput(terms); + }); +} + +/** +Given namespace-sorted terms, return an HTML list of each term with its evidence +code(s). +**/ +function buildVisualOutput(terms) { + var namespaces = Object.keys(terms), + termUI = "
"; + //loop through the namespaces and create a header for each namespace + namespaces.map(function(namespace) { + termUI = termUI + "

" + namespace + "

    "; + //loop through the terms in each namespace + terms[namespace].map(function(result) { + //create a new list entry for each GO term. + termUI = termUI + "
  • " + result.ontologyTerm.name + // we're also going to add the evidence codes for each GO term. Codes + // are used to explain _why_ a given gene is annotation with a GO term. + // Evidence codes come in an array (there could be more than one per GO + // term), so we have to loop through them too. + result.evidence.map(function(evidence) { + //add a span for each evidence code. + termUI = termUI + "" + evidence.code.code + ""; + }); + //close all the open elements so we have well-formed HTML. + termUI = termUI + "
  • "; + }); + termUI = termUI + "
" + }); + return termUI; +} + +/** +Given a set of InterMine results (GO terms associated with a single gene), +sort the terms by namespace and return the results sorted into named buckets. +**/ +function resultsToNamespaceBuckets(response) { + //we're going to sort our terms by namespace. + //here's a var to store each type of term in... + var terms = { + molecular_function: [], + cellular_component: [], + biological_process: [] + }; + //iterate through the results and group them by namespace + response[0].goAnnotation.map(function(result) { + //we don't need to store anything except the details in ontologyterm + //push each term into the correct box + terms[result.ontologyTerm.namespace].push(result); + }); + return terms; +} +``` + +##### Goodness, that's ugly! Let's make it look a little nicer. + +_Note: This section assumes some basic familiarity with [CSS](https://developer.mozilla.org/en-US/docs/Learn/CSS/Introduction_to_CSS) and [LESS](http://lesscss.org/). You can use CSS frameworks like Bootstrap if you prefer (Bootstrap 3 is on the window), but you'll still need to follow this guide and namespace + compile your CSS._ + +Head over to `src/style.less`. You'll notice that there have been a few lines pre-populated on your behalf - it might look something like this: + +```less +.bluegenesToolGOTerms { + // put all your css classes inside this block. Keeping them + // nested under the tool name prevents your styles from + // leaking out and affecting other elements by accident. + + // If you want to learn more about LESS, visit lesscss.org. + + //to build your css, run `lessc src/style.css dist/style.css --clean-css` + // in your terminal +} + +``` + +Nest all your css inside the main code block, between the two curly brackets `{}` - this ensures that _your_ css won't affect bluegenes or other tools by accident. + +We've added a few minimal style rules here to show the GO terms in a column layout: + +```less +.bluegenesToolGOTerms { + // put all your css classes inside this block. Keeping them + // nested under the tool name prevents your styles from + // leaking out and affecting other elements by accident. + // If you want to learn more about LESS, visit lesscss.org. + //to build your css, run `lessc src/style.css dist/style.css --clean-css` + // in your terminal + .namespaces { + display: flex; + flex-wrap:wrap; + + .namespace { + margin: 1em; + max-width:30%; + h3 {text-align:center;} + } + + .evidencecode { + margin: 0.2em; + } + } +} +``` + +In order for the less to affect the page, we need to compile it into CSS. In your console, run + +```bash +npm run less +``` + +Now, if you refresh your demo.html page, you should see the new css applied! You can also look at your compiled CSS if you wish - it's in `dist/style.css`. + +### Releasing your tool and using it in BlueGenes. + +At this point, you should have a basic functional tool that consumes InterMine data and looks nice when you visit http://localhost:3456 - well done! So you'll probably want to test it in BlueGenes now, right? You have two ways to do this: + +- for tools under development, like this one, the easiest thing to do is [use npm link to install your new tool](tools.md#development-tools) in the bluegenes tool folder. +- for tools that are release ready (perhaps because you've tested them via the npm link method above), you can [release the tool on npm](https://docs.npmjs.com/cli/publish). This will make the tool available for others. To install a tool that's released via NPM, see our [installing published tools guide](tools.md#published-tools). diff --git a/tools/docs/tool-api.md b/tools/docs/tool-api.md new file mode 100644 index 000000000..e540254d8 --- /dev/null +++ b/tools/docs/tool-api.md @@ -0,0 +1,129 @@ +# Bluegenes Tool API Spec + +BlueGenes is built in Clojure and ClojureScript, but we don't want to re-write all existing browser data vis and analysis tools, so we've built a way to integrate JavaScript tools into report pages. + +You can take existing JavaScript biology applications and provide a wrapper for them, allowing them to interact with BlueGenes. + +This document provides reference for the API specifications, but there is also a [tutorial to walk you through creating a wrapper for your tool](tool-api-tutorial.md). + +## App structure: + +``` +. ++-- myapp/ +| +-- dist/ +| +-- style.css (optional) +| +-- bundle.js (required) +| +-- src/ +| +-- style.less (optional, could be some other preprocessor too.) +| +-- index.js (optional but recommended) +| +-- config.json (required) +| +-- package.json (required) +| +-- demo.html (optional) +| +-- preview.png (optional) +``` + +You may optionally also have additional folders, including node_modules, if needed. They won't interfere. + + +### dist + +Put all of your prod-ready bundled files in here. Ideally this should be no more than two things: + +- **bundle.js**, contains your entire application, with all dependencies bundled in, excluding im.js which is available on the window. +- **style.css**, optional. Use if any additional styles are required. If your file isn't called style.css, make sure to specify the file name in config json. + +### src + +Where do the bundled files come from? Probably the src directory. This is the folder you'll be doing most of your work in. + +#### index.js + +This is the preferred entry point to build dist/bundle.js. May import external libraries or node modules if needed. [See bluegenesProtVista example ](https://github.com/intermine/bluegenesProtVista/tree/master/src). Make sure to export an object that matches your tool name and has a main method - e.g. for bluegenesProtVista, there is an exposed method called `bluegenesProtvista.main()`. + +The signature of the main method should look have the following signature: + +```javascript +var myApp.main = function(el, service, imEntity, state, config) { + // code to initialise your app here +} +``` + + +**el** - The id of a dom element where the tool will render + +**service** - An object representing an intermine service, like the following: + +```json +{ + "root": "www.flymine.org/query", + "token": "bananacakes" +} +``` + +**imEntity** + +An object representing the data passed to the app, e.g.: + +```json +{ + "class": "Gene", + "format": "id", + "value": 456 +} +``` + +To see a full example, look at how the method is called in [bluegenesProtVista's ui demo file](https://github.com/intermine/bluegenesProtVista/blob/master/demo.html) and how [index.js uses the values passed to it](https://github.com/intermine/bluegenesProtVista/blob/master/src/index.js) + +#### style.less +This is the preferred entry point to build dist/styles.css. If your tool has a stylesheet already, make sure to import the styles and wrap them in a parent class to ensure the styles are sandboxed and don't leak into another file. See [bluegenesProtVista's less file for an example](https://github.com/intermine/bluegenesProtVista/blob/master/src/style.less) and [the css it compiled to](https://github.com/intermine/bluegenesProtVista/blob/master/dist/style.css). + +### config.json + +This file provides bluegenes-specific config info. Some further config info is drawn from package.json as well. + +```json +{ + "accepts": ["id", "ids", "list", "lists", "records", "rows"], + "classes": ["Gene", "Protein", "*"], + "columnMapping" : {"Protein" : {"id" : "primaryAccession"}}, + "files" : { + "css" : "dist/style.css", + "js" : "dist/bundle.js" + }, + "toolName" : "Protein Features" +} +``` +#### Accepts: + +##### Currently supported in BlueGenes: +* id: a single database id + +##### Planned for the future: + +* ids: multiple database ids +* list: a single list name +* lists: multiple list names +* records: a raw result from POSTing to /query/results with format "jsonobjects" +* rows: a raw result from POSTing to /query/results with format "json" + +Plurality (i.e. id vs ids) will help to determine which context a tool can appear (report page, list analysis page) + +**classes** default to `*` if this tool isn't class / objectType specific. otherwise your tool might be specific to a certain class, e.g. a gene displayer. + +**columnMapping** is an important way to specify (or override) which columns should be passed to the tool by BlueGenes. As an example, for a gene tool, you might want to pass a symbol, OR a primaryIdentifier, or even secondaryIdentifier - and this might change depending in the InterMine that is fuelling the BlueGenes. Set a default likely value here, and in the future individual bluegenes administrators can override it if needed. + + +**files** - one file each for css and js, please. This should be the file bundled/built with all dependencies except/ imjs if needed. CSS is optional if the tool has no styles. + +**toolName** what would you want to see as a header for this tool? e.g. ProtVista might be called "Protein Features". + + +#### preview.png + +Optional preview image for the "app store" dashboard (when admins are selecting tools, this is the way to impress them!) + +## Other notes +* [imjs](https://www.npmjs.com/package/imjs) will be available on the window automatically. + +**Credits:** Thanks to [Josh](https://gist.github.com/joshkh/76091f1182d425934c1c5dbe2644d23a) and [Vivek](https://gist.github.com/vivekkrish/2e5e4128efbbf2014c194aae6b83d245) for early work on the Tool API proposal. diff --git a/tools/docs/tools.md b/tools/docs/tools.md new file mode 100644 index 000000000..8b13d649a --- /dev/null +++ b/tools/docs/tools.md @@ -0,0 +1,67 @@ +# BlueGenes Tool API + +The BlueGenes Tool API is designed to make it easy to run javascript-based tools in the BlueGenes non-js codebase. Anything that compiles to javascript is valid. + +## Installing tools + +Since BlueGenes tools are always JavaScript, we use npm (a popular JavaScript package manager) to manage the packages. You must have a recent version of npm and node installed. See our [requirements](https://github.com/intermine/bluegenes/blob/dev/README) for more info. + +### Published tools +Tools that conform to the [tool API spec](tool-api.md) may be published in [npm](https://www.npmjs.com/) under the tag [bluegenes-intermine-tool](https://www.npmjs.com/search?q=keywords:bluegenes-intermine-tool). To install a package that is already published: + +```bash +# make a tools directory somewhere on your computer +mkdir tools +cd tools +npm init -y # This creates package.json, where your tool list is stored. +npm install --save some-tool-name-here +``` + +This will install the tool into the `tools/node_modules/@intermine` directory, and save the tool to your package.json file. + +#### When launching BlueGenes via a JAR +The tool folder will automatically be picked up and displayed in any relevant pages, so long as you [configure InterMine so it knows where to look for the tools folder](https://intermine.readthedocs.io/en/latest/webapp/blue-genes/). + +#### When working locally with a lein-compiled BlueGenes +The default location for tools is the following path - you can modify it to elsewhere if you wish: https://github.com/yochannah/bluegenes/blob/tool-api-2018/config/defaults/config.edn#L3 + +#### Updating published tools + +If a tool's npm package is updated, all you need to do in order to pull the updates is run `npm update` from your tools folder, and all your packages will be updated. + +### Development tools + +If your tool is under development, you'll need to mimic it living in the `tools/node_modules/@intermine` directory. Your tool should comply with the [tool API guidelines](tool-api.md). + +1. In the folder where **your tool** is being developed, type `npm link`. This tells npm on your system that you have a local node package at this location. +2. In the bluegenes tool folder (which might be `bluegenes/tools`) type `npm link your-package-name` where your-package-name is the name defined for your package in its package.json. This creates a symbolic link to your package using [npm link](https://docs.npmjs.com/cli/link), so any updates you make in your package will automatically be mirrored in BlueGenes. +3. BlueGenes looks in your package.json to figure out which tools to show. Open package.json in the tools folder, and tell it to look for the latest version of your package. That might look something like this: + +```json +{ + "name": "tools", + "version": "1.0.0", + "description": "Tool API", + "dependencies": { + "my-awesome-new-tool-here": "latest", <---- Add a line that looks like this!! + "@intermine/bluegenes-cytoscape-interaction-network-viewer": "^1.1.0", + "@intermine/bluegenes-protvista": "^1.1.1" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Yo Yehudi", + "license": "ISC" +} +``` + +## Creating new tools + +You can create your own tools in JavaScript or any language that compiles to JavaScript (e.g. TypeScript, CoffeeScript, ClojureScript). We also have a [yeoman generator](https://github.com/intermine/generator-bluegenes-tool) that sets up the basics for you. See the [Tool API Guidelines](tool-api.md) for more details. + +## Roadmap + +Currently tools are only supported in report pages. Some of the planned work: + - Longer term we would like to add support for tools in list results pages. + - Support for global variables and events (i.e. for javascript libraries that aren't modern enough to export a single module). diff --git a/tools/package.json b/tools/package.json new file mode 100644 index 000000000..0c6850a8e --- /dev/null +++ b/tools/package.json @@ -0,0 +1,15 @@ +{ + "name": "tools", + "version": "1.0.0", + "description": "Tool API", + "dependencies": { + "@intermine/bluegenes-cytoscape-interaction-network-viewer": "^1.1.0", + "@intermine/bluegenes-protvista": "^1.1.1" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Yo Yehudi", + "license": "ISC" +}