Merge pull request #9261 from OlegZharkov/tools-view-port

implementing a global tools-view component + schema.org support
This commit is contained in:
Dannon
2020-03-18 11:22:56 -04:00
committed by GitHub
16 changed files with 2402 additions and 79 deletions
+88 -59
View File
@@ -1,33 +1,52 @@
<template>
<b-card>
<template v-slot:header>
<h4 class="mb-0">
Citations
<b-button
v-if="viewRender"
@click="toggleViewRender"
title="Show all in BibTeX format."
class="citations-to-bibtex"
>
<i class="fa fa-pencil-square-o"></i> Show BibTeX
</b-button>
<b-button v-else @click="toggleViewRender" title="Return to formatted citation list.">
<i class="fa fa-times"></i> Hide BibTeX
</b-button>
</h4>
</template>
<div v-if="source === 'histories'" class="infomessage">
When writing up your analysis, remember to include all references that should be cited in order to
completely describe your work. Also, please remember to
<a href="https://galaxyproject.org/citing-galaxy">cite Galaxy</a>.
</div>
<span v-if="viewRender" class="citations-formatted"> <p v-html="formattedReferences"></p> </span>
<pre v-else>
<div>
<b-card v-if="!simple">
<template v-slot:header>
<h4 class="mb-0">
Citations
<b-button
v-if="viewRender"
title="Show all in BibTeX format."
class="citations-to-bibtex"
@click="toggleViewRender"
>
<i class="fa fa-pencil-square-o"></i> Show BibTeX
</b-button>
<b-button v-else title="Return to formatted citation list." @click="toggleViewRender">
<i class="fa fa-times"></i> Hide BibTeX
</b-button>
</h4>
</template>
<div v-if="source === 'histories'" class="infomessage">
When writing up your analysis, remember to include all references that should be cited in order to
completely describe your work. Also, please remember to
<a href="https://galaxyproject.org/citing-galaxy">cite Galaxy</a>.
</div>
<span v-if="viewRender" class="citations-formatted">
<p v-html="formattedReferences"></p>
</span>
<pre v-else>
<code class="citations-bibtex">
{{ content }}
</code>
</pre>
</b-card>
</b-card>
<div v-else-if="citations.length">
<b-btn v-b-toggle="id" variant="primary">Citations</b-btn>
<b-collapse
:id="id"
class="mt-2"
@show="$emit('show')"
@shown="$emit('shown')"
@hide="$emit('hide')"
@hidden="$emit('hidden')"
>
<b-card>
<p v-html="formattedReferences"></p>
</b-card>
</b-collapse>
</div>
</div>
</template>
<script>
import _ from "underscore";
@@ -55,29 +74,40 @@ export default {
type: Boolean,
requried: false,
default: true
},
simple: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
citations: [],
content: "",
errors: []
errors: [],
showCollapse: false
};
},
computed: {
formattedReferences: function() {
formattedReferences() {
return this.citations.reduce(
(a, b) => a.concat(`<p class="formatted-reference">${this.formattedReference(b)}</p>`),
""
);
}
},
created: function() {
updated() {
this.$nextTick(() => {
this.$emit("rendered");
});
},
created() {
axios
.get(`${getAppRoot()}api/${this.source}/${this.id}/citations`)
.then(response => {
this.content = "";
for (const rawCitation of response.data) {
response.data.forEach(rawCitation => {
try {
const citation = {
fields: {},
@@ -87,68 +117,67 @@ export default {
if (parsed) {
parsed = _.first(parsed);
citation.entryType = parsed.entryType || undefined;
for (const key in parsed.entryTags) {
Object.keys(parsed.entryTags).forEach(key => {
citation.fields[key.toLowerCase()] = parsed.entryTags[key];
}
});
}
this.citations.push(citation);
this.content += rawCitation.content;
} catch (err) {
console.warn("Error parsing bibtex: " + err);
console.warn(`Error parsing bibtex: ${err}`);
}
}
});
})
.catch(e => {
console.error(e);
});
},
methods: {
formattedReference: function(citation) {
const entryType = citation.entryType;
const fields = citation.fields;
formattedReference(citation) {
const { entryType, fields } = citation;
let ref = "";
const authorsAndYear = `${this._asSentence(
const authorsAndYear = `${this.asSentence(
(fields.author ? fields.author : "") + (fields.year ? ` (${fields.year})` : "")
)} `;
const title = fields.title || "";
const pages = fields.pages ? `pp. ${fields.pages}` : "";
const address = fields.address;
if (entryType == "article") {
const { address } = fields;
if (entryType === "article") {
const volume =
(fields.volume ? fields.volume : "") +
(fields.number ? ` (${fields.number})` : "") +
(pages ? `, ${pages}` : "");
ref = `${authorsAndYear +
this._asSentence(title) +
this.asSentence(title) +
(fields.journal ? `In <em>${fields.journal}, ` : "") +
this._asSentence(volume) +
this._asSentence(fields.address)}</em>`;
} else if (entryType == "inproceedings" || entryType == "proceedings") {
this.asSentence(volume) +
this.asSentence(fields.address)}</em>`;
} else if (entryType === "inproceedings" || entryType === "proceedings") {
ref = `${authorsAndYear +
this._asSentence(title) +
this.asSentence(title) +
(fields.booktitle ? `In <em>${fields.booktitle}, ` : "") +
(pages ? pages : "") +
(pages || "") +
(address ? `, ${address}` : "")}.</em>`;
} else if (entryType == "mastersthesis" || entryType == "phdthesis") {
} else if (entryType === "mastersthesis" || entryType === "phdthesis") {
ref =
authorsAndYear +
this._asSentence(title) +
this.asSentence(title) +
(fields.howpublished ? `${fields.howpublished}. ` : "") +
(fields.note ? `${fields.note}.` : "");
} else if (entryType == "techreport") {
} else if (entryType === "techreport") {
ref =
authorsAndYear +
this._asSentence(title) +
this._asSentence(fields.institution) +
this._asSentence(fields.number) +
this._asSentence(fields.type);
} else if (entryType == "book" || entryType == "inbook" || entryType == "incollection") {
ref = `${authorsAndYear} ${this._formatBookInfo(fields)}`;
this.asSentence(title) +
this.asSentence(fields.institution) +
this.asSentence(fields.number) +
this.asSentence(fields.type);
} else if (entryType === "book" || entryType === "inbook" || entryType === "incollection") {
ref = `${authorsAndYear} ${this.formatBookInfo(fields)}`;
} else {
ref = `${authorsAndYear} ${this._asSentence(title)}${this._asSentence(
ref = `${authorsAndYear} ${this.asSentence(title)}${this.asSentence(
fields.howpublished
)}${this._asSentence(fields.note)}`;
)}${this.asSentence(fields.note)}`;
}
if (fields.doi) {
ref += `[<a href="https://doi.org/${fields.doi}" target="_blank">doi:${fields.doi}</a>]`;
@@ -158,7 +187,7 @@ export default {
}
return convertLaTeX({ onError: (error, latex) => `{${stringifyLaTeX(latex)}}` }, ref);
},
_formatBookInfo: function(fields) {
formatBookInfo(fields) {
let info = "";
if (fields.chapter) {
info += `${fields.chapter} in `;
@@ -186,10 +215,10 @@ export default {
}
return `${info}.`;
},
_asSentence: function(str) {
asSentence(str) {
return str && str.trim() ? `${str}. ` : "";
},
toggleViewRender: function() {
toggleViewRender() {
this.viewRender = !this.viewRender;
}
}
@@ -84,6 +84,14 @@ export default {
document.getElementById("center").style.transition = "";
}, 250);
}
},
hide() {
this.show = false;
document.getElementById("center").style.transition = `${this.side} 200ms linear`;
document.getElementById("center").style[this.side] = "0";
setTimeout(() => {
document.getElementById("center").style.transition = "";
}, 250);
}
}
};
@@ -0,0 +1,29 @@
import ToolsJson from "./ToolsJson";
// test response
import testToolsListResponse from "../testData/toolsList";
import MockAdapter from "axios-mock-adapter";
import axios from "axios";
import Vue from "vue";
import { mount } from "@vue/test-utils";
describe("ToolsView/ToolsView.vue", () => {
let wrapper, axiosMock;
const defaultSchemaElementTag = "application/ld+json";
beforeEach(async () => {
axiosMock = new MockAdapter(axios);
wrapper = mount(ToolsJson);
axiosMock.onGet("/api/tools?tool_help=True").reply(200, testToolsListResponse);
await Vue.nextTick();
});
it("schema.org script element is created", async () => {
await Vue.nextTick();
const tools = wrapper.vm.createToolsJson(testToolsListResponse);
const schemaElement = document.getElementById("schema-json");
const schemaText = JSON.parse(schemaElement.text);
assert(tools["@graph"].length === 84, "tools are not parsed correctly!");
assert(schemaText["@graph"].length === 84, "tools are not correctly presented in schema script tag!");
assert(schemaElement.type === defaultSchemaElementTag, "element type is not correct!");
});
});
@@ -0,0 +1,60 @@
<template>
<div />
</template>
<script>
import { getAppRoot } from "onload/loadConfig";
import axios from "axios";
export default {
data() {
return { schemaTagObj: {} };
},
methods: {
createToolsJson(tools) {
function extractSections(acc, section) {
function extractTools(_acc, tool) {
return tool.name
? [
..._acc,
{
"@type": "SoftwareApplication",
operatingSystem: "Any",
applicationCategory: "Web application",
name: tool.name,
description: tool.help || tool.description,
softwareVersion: tool.version,
url: getAppRoot() + String(tool.link).substring(1)
}
]
: _acc;
}
if ("elems" in section) {
return acc.concat(section.elems.reduce(extractTools, []));
} else {
return acc;
}
}
return {
"@context": "http://schema.org",
"@graph": tools.reduce(extractSections, [])
};
}
},
created() {
axios
.get(`${getAppRoot()}api/tools?tool_help=True`)
.then(response => {
this.schemaTagObj = this.createToolsJson(response.data);
const el = document.createElement("script");
el.id = "schema-json";
el.type = "application/ld+json";
el.text = JSON.stringify(this.schemaTagObj);
document.head.appendChild(el);
})
.catch(error => {
console.error(error);
});
}
};
</script>
@@ -0,0 +1,82 @@
/* global expect */
import ToolsView from "./ToolsView";
import { shallowMount, mount, createLocalVue } from "@vue/test-utils";
import _l from "utils/localization";
import Vue from "vue";
// test response
import testToolsListResponse from "./testData/toolsList";
import testCitation from "./testData/citation";
import MockAdapter from "axios-mock-adapter";
import axios from "axios";
describe("ToolsView/ToolsView.vue", () => {
const localVue = createLocalVue();
localVue.filter("localize", value => _l(value));
let wrapper, emitted, axiosMock;
beforeEach(async () => {
axiosMock = new MockAdapter(axios);
wrapper = mount(ToolsView);
emitted = wrapper.emitted();
axiosMock.onGet("/api/tools?tool_help=True").reply(200, testToolsListResponse);
axiosMock.onGet(new RegExp(`./*/citations`)).reply(200, testCitation);
await Vue.nextTick();
await Vue.nextTick();
});
afterEach(() => {
axiosMock.restore();
});
it("should render infinite scroll div", async () => {
expect(wrapper.html()).contain('<div infinite-scroll-disabled="busy">');
});
it("should return defined number of tools", async () => {
assert(wrapper.vm.getToolsNumber() === 84, "Tools Get Response is not parsed correctly!");
});
it("should render only specific number of tools, equal to current buffer", async () => {
let buttons = wrapper.findAll('[type="button"]').filter(button => button.text() === "Info");
// one 'info' button per tool
assert(wrapper.vm.buffer.length === buttons.length, "Number of 'info' buttons do not equal the buffer size!");
});
it("should open modal on button click", async () => {
// findAll() returns WrapperArray, thus regular array.find() won't work
let infoButton = wrapper
.findAll('[type="button"]')
.filter(button => button.text() === "Info")
.at(0);
const modalId = "modal--" + infoButton.attributes().index;
const modal = wrapper.find("#" + modalId);
assert(modal.isVisible() === false, "modal is visible before the click!");
infoButton.trigger("click");
await Vue.nextTick();
assert(modal.isVisible(), "'Info' button didn't open a modal!");
});
it("citation should open on click", async () => {
await Vue.nextTick();
await Vue.nextTick();
await Vue.nextTick();
let infoButton = wrapper
.findAll('[type="button"]')
.filter(button => button.text() === "Citations")
.at(0);
const citation = wrapper.find("#" + infoButton.attributes("aria-controls"));
assert(citation.isVisible() === false, "citation is visible before being triggered!");
assert(infoButton.attributes("aria-expanded") === "false", "citation is expanded before being triggered!");
infoButton.trigger("click");
await Vue.nextTick();
assert(infoButton.attributes("aria-expanded") === "true", "citation field did not expand!");
assert(citation.isVisible(), "citation is not visible, after being triggered!");
});
});
@@ -0,0 +1,209 @@
<template>
<div v-infinite-scroll="loadMore" infinite-scroll-disabled="busy">
<h2 class="mb-3" style="text-align: center">
<span id="tools-view">Consolidated view of {{ tools.length }} available tools.</span>
</h2>
<div v-if="!loading">
<b-container fluid class="mb-4">
<b-row class="justify-content-center">
<b-col md="6">
<b-form-group description="Search for strings or regular expressions">
<b-input-group>
<b-form-input
v-model="filterText"
placeholder="Type to Search"
@keyup.native="filter('filterByText')"
@keyup.esc.native="filterText = ''"
/>
<b-input-group-append>
<b-btn :disabled="!filter" @click="filter = ''">Clear (esc)</b-btn>
</b-input-group-append>
</b-input-group>
</b-form-group>
</b-col>
</b-row>
</b-container>
<isotope
ref="iso"
:options="isoOptions"
:list="buffer"
style="margin: 0 auto;"
@filter="filterOption = arguments[2]"
>
<b-card v-for="(info, index) in buffer" :key="index" ref="cards" class="m-2" style="width: 23rem;">
<template v-slot:header>
<div>
<b-link :href="info.url" target="_blank">
<h4 class="tools-view-name">{{ info.name }}</h4>
</b-link>
<b-badge class="tools-view-section">{{ info.section }}</b-badge>
</div>
</template>
<p class="card-text" v-html="helpSummary(info.help) || info.description" />
<p class="card-text">
<b-btn :index="index" v-b-modal="'modal-' + '-' + index">Info</b-btn>
<b-modal :id="'modal-' + '-' + index" centered ok-only :static="true" :title="info.name">
<b>{{ info.version + " / " + info.id }}</b>
<p>{{ info.description }}</p>
<p v-html="info.help"></p>
</b-modal>
</p>
<Citations
:id="info.id"
simple
source="tools"
@rendered="layout"
@show="show(index)"
@shown="shown(index)"
@hidden="hidden(index)"
/>
</b-card>
</isotope>
</div>
</div>
</template>
<script>
import { getAppRoot } from "onload/loadConfig";
import infiniteScroll from "vue-infinite-scroll";
import isotope from "vueisotope";
import axios from "axios";
import Citations from "components/Citations.vue";
import { setTimeout } from "timers";
export default {
components: {
Citations,
isotope
},
directives: { infiniteScroll },
props: {
transitionDuration: {
type: Number,
default: 200
}
},
data() {
return {
tools: [],
buffer: [],
filterOption: null,
filterText: "",
busy: false,
loading: true
};
},
computed: {
isoOptions() {
return {
transitionDuration: this.transitionDuration,
masonry: {
fitWidth: true
},
getFilterData: {
filterByText: el => {
const re = new RegExp(this.filterText, "i");
return el.name.match(re) || el.description.match(re) || el.help.match(re);
}
}
};
}
},
created() {
axios
.get(`${getAppRoot()}api/tools?tool_help=True`)
.then(response => {
this.initialize(response.data);
})
.catch(error => {
console.error(error);
});
},
methods: {
show(index) {
this.$refs.cards[index].style.zIndex = "1";
},
shown(index) {
this.layout();
setTimeout(() => {
this.$refs.cards[index].style.zIndex = "auto";
}, 200);
},
hidden(index) {
this.layout();
},
layout() {
this.$refs.iso.layout();
},
filter(key) {
this.$refs.iso.filter(key);
},
toolsExtracted(tools) {
function extractSections(acc, section) {
function extractTools(_acc, tool) {
return tool.name
? [
..._acc,
{
id: tool.id,
name: tool.name,
section: section.name,
description: tool.description,
url: getAppRoot() + String(tool.link).substring(1),
version: tool.version,
help: tool.help
}
]
: _acc;
}
if ("elems" in section) {
return acc.concat(section.elems.reduce(extractTools, []));
}
return acc;
}
return tools
.reduce(extractSections, [])
.map(a => [Math.random(), a])
.sort((a, b) => a[0] - b[0])
.map(a => a[1]);
},
loadMore() {
if (this.buffer.length < this.tools.length) {
this.busy = true;
setTimeout(() => {
const start = this.buffer.length;
const end = start + 10;
const newItems = this.tools.slice(start, end);
this.buffer = this.buffer.concat(newItems);
this.busy = false;
}, 100);
}
},
helpSummary(help) {
const parser = new DOMParser();
const helpDoc = parser.parseFromString(help, "text/html");
const xpath = "//strong[text()='What it does']/../following-sibling::*";
const match = helpDoc.evaluate(xpath, helpDoc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
.singleNodeValue;
if (match) {
return match.innerHTML;
}
const helpText = helpDoc.documentElement.textContent;
if (helpText) {
return helpText.substring(0, helpText.indexOf("\n\n"));
}
return null;
},
getToolsNumber() {
return this.tools.length;
},
initialize(tools) {
this.tools = this.toolsExtracted(tools);
this.buffer = this.tools.slice(0, 20);
this.loading = false;
}
}
};
</script>
@@ -0,0 +1 @@
[{"content": " @article{Blankenberg_2011, title={Making whole genome multiple alignments usable for biologists}, volume={27}, ISSN={1367-4803}, url={http://dx.doi.org/10.1093/bioinformatics/btr398}, DOI={10.1093/bioinformatics/btr398}, number={17}, journal={Bioinformatics}, publisher={Oxford University Press (OUP)}, author={Blankenberg, Daniel and Taylor, James and Nekrutenko, Anton}, year={2011}, month={Jul}, pages={2426\u00e2\u0080\u00932428}}\n", "format": "bibtex"}]
File diff suppressed because one or more lines are too long
@@ -33,6 +33,8 @@ import HistoryView from "components/HistoryView.vue";
import WorkflowInvocationReport from "components/WorkflowInvocationReport.vue";
import WorkflowRun from "components/Workflow/Run/WorkflowRun.vue";
import RecentInvocations from "components/User/RecentInvocations.vue";
import ToolsView from "components/ToolsView/ToolsView.vue";
import ToolsJson from "components/ToolsView/ToolsSchemaJson/ToolsJson.vue";
import HistoryList from "mvc/history/history-list";
import PluginList from "components/PluginList.vue";
import QueryStringParsing from "utils/query-string-parsing";
@@ -51,6 +53,8 @@ export const getAnalysisRouter = Galaxy =>
routes: {
"(/)(#)(_=_)": "home",
"(/)root*": "home",
"(/)tools/view": "show_tools_view",
"(/)tools/json": "show_tools_json",
"(/)tours(/)(:tour_id)": "show_tours",
"(/)user(/)": "show_user",
"(/)user(/)cloud_auth": "show_cloud_auth",
@@ -242,6 +246,15 @@ export const getAnalysisRouter = Galaxy =>
this._display_vue_helper(HistoryImport);
},
show_tools_view: function() {
this.page.toolPanel.getVueComponent().hide();
this.page.panels.right.hide();
this._display_vue_helper(ToolsView);
},
show_tools_json: function() {
this._display_vue_helper(ToolsJson);
},
show_histories_permissions: function() {
this.page.display(
new FormWrapper.View({
+3
View File
@@ -31,6 +31,7 @@
"glob": "^7.1.4",
"handsontable": "^2.0.0",
"imask": "^4.1.5",
"isotope-layout": "^3.0.6",
"jquery": "2",
"jquery-migrate": "~1.4",
"jquery-mousewheel": "^3.1.13",
@@ -50,9 +51,11 @@
"toastr": "^2.1.4",
"underscore": "^1.9.2",
"vue": "^2.6.11",
"vue-infinite-scroll": "^2.0.2",
"vue-multiselect": "^2.1.0",
"vue-router": "^3.1.5",
"vue-rx": "^6.2.0",
"vueisotope": "^3.1.2",
"vuex": "^3.1.2",
"vuex-cache": "^3.1.0"
},
+67 -4
View File
@@ -4748,6 +4748,11 @@ des.js@^1.0.0:
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
desandro-matches-selector@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz#717beed4dc13e7d8f3762f707a6d58a6774218e1"
integrity sha1-cXvu1NwT59jzdi9wem1YpndCGOE=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
@@ -5397,6 +5402,11 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
ev-emitter@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ev-emitter/-/ev-emitter-1.1.1.tgz#8f18b0ce5c76a5d18017f71c0a795c65b9138f2a"
integrity sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q==
eventemitter3@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
@@ -5949,6 +5959,13 @@ fined@^1.0.1:
object.pick "^1.2.0"
parse-filepath "^1.0.1"
fizzy-ui-utils@^2.0.0, fizzy-ui-utils@^2.0.4:
version "2.0.7"
resolved "https://registry.yarnpkg.com/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz#7df45dcc4eb374a08b65d39bb9a4beedf7330505"
integrity sha512-CZXDVXQ1If3/r8s0T+v+qVeMshhfcuq0rqIFgJnrtd+Bu8GmDmqMjntjUePypVtjHXKJ6V4sw9zeyox34n9aCg==
dependencies:
desandro-matches-selector "^2.0.0"
flagged-respawn@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.0.tgz#4e79ae9b2eb38bf86b3bb56bf3e0a56aa5fcabd7"
@@ -6213,6 +6230,11 @@ get-own-enumerable-property-symbols@^3.0.0:
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203"
integrity sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg==
get-size@^2.0.0, get-size@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/get-size/-/get-size-2.0.3.tgz#54a1d0256b20ea7ac646516756202769941ad2ef"
integrity sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q==
get-stdin@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@@ -7770,6 +7792,17 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
isotope-layout@^3.0.1, isotope-layout@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/isotope-layout/-/isotope-layout-3.0.6.tgz#d5636ec74d68e7e88fa5166fe6396858db00f432"
integrity sha512-z2ZKablhocXhoNyWwzJPFd7u7FWbYbVJA51Nvsqsod8jH2ExGc1SwDsSWKE54e3PhXzqf2yZPhFSq/c2MR1arw==
dependencies:
desandro-matches-selector "^2.0.0"
fizzy-ui-utils "^2.0.4"
get-size "^2.0.0"
masonry-layout "^4.1.0"
outlayer "^2.1.0"
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -8388,7 +8421,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.0.0, lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10:
lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@@ -8588,6 +8621,14 @@ markdown-to-jsx@^6.9.1:
prop-types "^15.6.2"
unquote "^1.1.0"
masonry-layout@^4.1.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/masonry-layout/-/masonry-layout-4.2.2.tgz#d57b44af13e601bfcdc423f1dd8348b5524de348"
integrity sha512-iGtAlrpHNyxaR19CvKC3npnEcAwszXoyJiI8ARV2ePi7fmYhIud25MHK8Zx4P0LCC4d3TNO9+rFa1KoK1OEOaA==
dependencies:
get-size "^2.0.2"
outlayer "^2.1.0"
matchdep@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"
@@ -9759,6 +9800,15 @@ osenv@0, osenv@^0.1.0, osenv@^0.1.4:
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
outlayer@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/outlayer/-/outlayer-2.1.1.tgz#29863b6de10ea5dadfffcadfa0d728907387e9a2"
integrity sha1-KYY7beEOpdrf/8rfoNcokHOH6aI=
dependencies:
ev-emitter "^1.0.0"
fizzy-ui-utils "^2.0.0"
get-size "^2.0.2"
p-defer@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
@@ -14116,6 +14166,11 @@ vue-hot-reload-api@^2.3.0:
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.1.tgz#b2d3d95402a811602380783ea4f566eb875569a2"
integrity sha512-AA86yKZ5uOKz87/q1UpngEXhbRkaYg1b7HMMVRobNV1IVKqZe8oLIzo6iMocVwZXnYitlGwf2k4ZRLOZlS8oPQ==
vue-infinite-scroll@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/vue-infinite-scroll/-/vue-infinite-scroll-2.0.2.tgz#ca37a91fe92ee0ad3b74acf8682c00917144b711"
integrity sha512-n+YghR059YmciANGJh9SsNWRi1YZEBVlODtmnb/12zI+4R72QZSWd+EuZ5mW6auEo/yaJXgxzwsuhvALVnm73A==
vue-loader@^15.8.3:
version "15.8.3"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.8.3.tgz#857cb9e30eb5fc25e66db48dce7e4f768602a23c"
@@ -14258,6 +14313,14 @@ vue@^2.6.9:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==
vueisotope@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/vueisotope/-/vueisotope-3.1.2.tgz#df9d3e32d9770ff97a409d6baca68ec830f03ae7"
integrity sha512-mPlHr+YT1kaXXPLzAEwLxgE8Q3gauqj7hfs1U0ww2oeO9cYCdI8sWbJL1iNRDZTSd6p7Lks+6el+FwWcnXLHvQ==
dependencies:
isotope-layout "^3.0.1"
lodash "^4.13.1"
vuex-cache@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vuex-cache/-/vuex-cache-3.1.0.tgz#aad576cfb39b325cc7a43ea9a42414e356374c4b"
@@ -14378,9 +14441,9 @@ webpack-dev-server@^3.1.14:
yargs "12.0.2"
webpack-dev-server@^3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.10.1.tgz#1ff3e5cccf8e0897aa3f5909c654e623f69b1c0e"
integrity sha512-AGG4+XrrXn4rbZUueyNrQgO4KGnol+0wm3MPdqGLmmA+NofZl3blZQKxZ9BND6RDNuvAK9OMYClhjOSnxpWRoA==
version "3.10.3"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz#f35945036813e57ef582c2420ef7b470e14d3af0"
integrity sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==
dependencies:
ansi-html "0.0.7"
bonjour "^3.5.0"
+8 -1
View File
@@ -1919,7 +1919,7 @@ class Tool(Dictifiable):
os.remove(temp_file)
return tarball_archive
def to_dict(self, trans, link_details=False, io_details=False):
def to_dict(self, trans, link_details=False, io_details=False, tool_help=False):
""" Returns dict of tool. """
# Basic information
@@ -1969,6 +1969,13 @@ class Tool(Dictifiable):
# FIXME: the Tool class should declare directly, instead of ad hoc inspection
regular_form = tool_class == Tool or isinstance(self, (DatabaseOperationTool, InteractiveTool))
tool_dict["form_style"] = "regular" if regular_form else "special"
if tool_help:
# create tool help
help_txt = ''
if self.help:
help_txt = self.help.render(static_path=self.app.url_for('/static'), host_url=self.app.url_for('/', qualified=True))
help_txt = unicodify(help_txt)
tool_dict['help'] = help_txt
return tool_dict
+16 -11
View File
@@ -1050,24 +1050,29 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
if elt:
yield elt
def get_tool_to_dict(self, trans, tool):
def get_tool_to_dict(self, trans, tool, tool_help=False):
"""Return tool's to_dict.
Use cache if present, store to cache otherwise.
Note: The cached tool's to_dict is specific to the calls from toolbox.
"""
to_dict = None
if not trans.user_is_admin:
to_dict = self._tool_to_dict_cache.get(tool.id, None)
if not tool_help:
to_dict = self._tool_to_dict_cache.get(tool.id, None)
if not to_dict:
to_dict = tool.to_dict(trans, link_details=True)
self._tool_to_dict_cache[tool.id] = to_dict
to_dict = tool.to_dict(trans, link_details=True, tool_help=tool_help)
if not tool_help:
self._tool_to_dict_cache[tool.id] = to_dict
else:
to_dict = self._tool_to_dict_cache_admin.get(tool.id, None)
if not tool_help:
to_dict = self._tool_to_dict_cache_admin.get(tool.id, None)
if not to_dict:
to_dict = tool.to_dict(trans, link_details=True)
self._tool_to_dict_cache_admin[tool.id] = to_dict
to_dict = tool.to_dict(trans, link_details=True, tool_help=tool_help)
if not tool_help:
self._tool_to_dict_cache_admin[tool.id] = to_dict
return to_dict
def to_dict(self, trans, in_panel=True, **kwds):
def to_dict(self, trans, in_panel=True, tool_help=False, **kwds):
"""
Create a dictionary representation of the toolbox.
Uses primitive cache for toolbox-specific tool 'to_dict's.
@@ -1078,9 +1083,9 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
for elt in panel_elts:
# Only use cache for objects that are Tools.
if hasattr(elt, "tool_type"):
rval.append(self.get_tool_to_dict(trans, elt))
rval.append(self.get_tool_to_dict(trans, elt, tool_help=tool_help))
else:
kwargs = dict(trans=trans, link_details=True, toolbox=self)
kwargs = dict(trans=trans, link_details=True, tool_help=tool_help, toolbox=self)
rval.append(elt.to_dict(**kwargs))
else:
filter_method = self._build_filter_method(trans)
@@ -1088,7 +1093,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
tool = filter_method(tool, panel_item_types.TOOL)
if not tool:
continue
rval.append(self.get_tool_to_dict(trans, tool))
rval.append(self.get_tool_to_dict(trans, tool, tool_help=tool_help))
return rval
def _lineage_in_panel(self, panel_dict, tool=None, tool_lineage=None):
+4 -3
View File
@@ -69,18 +69,19 @@ class ToolSection(Dictifiable, HasPanelItems):
copy.elems = self.elems.copy()
return copy
def to_dict(self, trans, link_details=False, toolbox=None):
def to_dict(self, trans, link_details=False, tool_help=False, toolbox=None):
""" Return a dict that includes section's attributes. """
section_dict = super(ToolSection, self).to_dict()
section_elts = []
kwargs = dict(
trans=trans,
link_details=link_details
link_details=link_details,
tool_help=tool_help
)
for elt in self.elems.values():
if hasattr(elt, "tool_type") and toolbox:
section_elts.append(toolbox.get_tool_to_dict(trans, elt))
section_elts.append(toolbox.get_tool_to_dict(trans, elt, tool_help=tool_help))
else:
section_elts.append(elt.to_dict(**kwargs))
section_dict['elems'] = section_elts
+2 -1
View File
@@ -57,6 +57,7 @@ class ToolsController(BaseAPIController, UsesVisualizationMixin):
trackster = util.string_as_bool(kwds.get('trackster', 'False'))
q = kwds.get('q', '')
tool_id = kwds.get('tool_id', '')
tool_help = util.string_as_bool(kwds.get('tool_help', 'False'))
# Find whether to search.
if q:
@@ -88,7 +89,7 @@ class ToolsController(BaseAPIController, UsesVisualizationMixin):
# Return everything.
try:
return self.app.toolbox.to_dict(trans, in_panel=in_panel, trackster=trackster)
return self.app.toolbox.to_dict(trans, in_panel=in_panel, trackster=trackster, tool_help=tool_help)
except Exception:
raise exceptions.InternalServerError("Error: Could not convert toolbox to dictionary")
+2
View File
@@ -123,6 +123,8 @@ def app_factory(global_conf, load_app_kwds={}, **kwargs):
webapp.add_client_route('/admin/quotas', 'admin')
webapp.add_client_route('/admin/form/{form_id}', 'admin')
webapp.add_client_route('/admin/api_keys', 'admin')
webapp.add_client_route('/tools/view')
webapp.add_client_route('/tools/json')
webapp.add_client_route('/tours')
webapp.add_client_route('/tours/{tour_id}')
webapp.add_client_route('/user')