diff --git a/api/controllers/app.js b/api/controllers/app.js index f839678a..f5fa226a 100644 --- a/api/controllers/app.js +++ b/api/controllers/app.js @@ -11,6 +11,7 @@ const config = require('../config'); const db = require('../models'); const common = require('../common'); const provenance = require('../lib/provenance'); +const e = require('express'); function canedit(user, rec) { if(user) { @@ -37,9 +38,9 @@ function canedit(user, rec) { * @apiSuccess {Object} List of apps (maybe limited / skipped) and total count */ router.get('/', common.jwt({credentialsRequired: false}), (req, res, next)=>{ - var skip = req.query.skip||0; - let limit = req.query.limit||100; - var ands = []; + const skip = req.query.skip||0; + const limit = req.query.limit||100; + const ands = []; if(req.query.find) ands.push(JSON.parse(req.query.find)); common.getprojects(req.user, (err, project_ids)=>{ @@ -93,7 +94,7 @@ router.get('/query', common.jwt({credentialsRequired: false}), (req, res, next)= //and get *all* apps (minus some heavy/unnecessary stuff) common.getprojects(req.user, async (err, project_ids)=>{ if(err) return next(err); - const apps = await db.Apps.find({ + let findQuery = { removed: false, $or: [ //if projects is set, user need to have access to it @@ -105,54 +106,115 @@ router.get('/query', common.jwt({credentialsRequired: false}), (req, res, next)= {projects: null}, //if projects is set to null, it's available to everyoone {projects: {$exists: false}}, //if projects not set, it's availableo to everyone ] - }) + }; + if(req.query.find) findQuery = {$and: [findQuery, JSON.parse(req.query.find)]}; + + let datatype_ids = []; + + if(req.query.includeIncompatible === 'true') { + // When incompatible flag is true, remove the datatype filter + if(findQuery.$and) { + findQuery.$and = findQuery.$and.filter(query => !query["inputs.datatype"]); + } + datatype_ids = JSON.parse(req.query.find)?.['inputs.datatype']?.['$in']; + } + + let apps = await db.Apps.find(findQuery) .select('-config -stats.gitinfo -contributors') //cut things we don't need //we want to search into datatype name/desc (desc might be too much?) .populate('inputs.datatype', 'name desc') .populate('outputs.datatype', 'name desc') .lean(); - if(!req.query.q) return res.json(apps); //if not query is set, return everything - const queryTokens = req.query.q.toLowerCase().split(" "); - - //then construct list of tokens for each app to search by - apps.forEach(app=>{ - let tokens = [ - app.name, - app.github_branch, - app.github, - app.desc, - app.desc_override, - app.doi, - ...app.tags - ]; - app.inputs.forEach(input=>{ - tokens = [...tokens, ...input.datatype_tags, input.datatype.name, input.datatype.desc]; - }); - app.outputs.forEach(output=>{ - tokens = [...tokens, ...output.datatype_tags, output.datatype.name, output.datatype.desc]; - }); - tokens = tokens.filter(token=>!!token).map(token=>token.toLowerCase()); + console.log("found apps", apps.length, "query", findQuery); + + if(!req.query.q && !req.query.includeIncompatible) { + return res.json(apps); + } //if not query is set, return everything + + + if(req.query.q && !req.query.includeIncompatible) { + const queryTokens = req.query.q.toLowerCase().split(" "); + //then construct list of tokens for each app to search by + apps.forEach(app=>{ + let tokens = [ + app.name, + app.github_branch, + app.github, + app.desc, + app.desc_override, + app.doi, + ...app.tags + ]; + app.inputs.forEach(input=>{ + tokens = [...tokens, ...input.datatype_tags, input.datatype.name, input.datatype.desc]; + }); + app.outputs.forEach(output=>{ + tokens = [...tokens, ...output.datatype_tags, output.datatype.name, output.datatype.desc]; + }); + tokens = tokens.filter(token=>!!token).map(token=>token.toLowerCase()); - //let's just store it as part of the app - app._tokens = tokens.join(" "); - }); + //let's just store it as part of the app + app._tokens = tokens.join(" "); + }); - //then filter apps using _tokens - const filtered = apps.filter(app=>{ - //for each query token, make sure all token matches somewhere in _tokens - let match = true; - queryTokens.forEach(token=>{ - if(!match) return; //we already know it won't match - if(!app._tokens.includes(token)) match = false; + //then filter apps using _tokens + const filtered = apps.filter(app=>{ + //for each query token, make sure all token matches somewhere in _tokens + let match = true; + queryTokens.forEach(token=>{ + if(!match) return; //we already know it won't match + if(!app._tokens.includes(token)) match = false; + }); + return match; }); - return match; - }); - //remove _tokens from the apps to reduce returning weight a bit - filtered.forEach(app=>{ delete app._tokens; }); + //remove _tokens from the apps to reduce returning weight a bit + apps = filtered.forEach(app=>{ delete app._tokens; }); + } + + // if includeIncompatible then include with app.compatible = false / true based on datatype_ids + if(req.query.includeIncompatible === 'true' && !req.query.q) { + + const datasetIds = req.query.datasetIds ? JSON.parse(req.query.datasetIds) : []; + const datasets = await db.Datasets.find({$or: [{datatype: {$in: datatype_ids}}, {_id: {$in: datasetIds}}]}).lean(); + + + apps.forEach(app => { + let missingInputIDs = []; + let match = true; + + app.inputs.forEach(input => { + if(input.optional) return; + + let matching_dataset = datasets.find(dataset => { + if(!input.datatype) return false; + if(dataset.datatype.toString() !== input.datatype._id.toString()) return false; + + let match_tag = true; + if(dataset.datatype_tags && input.datatype_tags) { + input.datatype_tags.forEach(tag => { + if(tag[0] === "!" && dataset.datatype_tags.includes(tag.substring(1))) match_tag = false; + if(tag[0] !== "!" && !dataset.datatype_tags.includes(tag)) match_tag = false; + }); + } + return match_tag; + }); + + if(!matching_dataset) { + missingInputIDs.push(input._id); + match = false; + } + }); + + app.compatible = match; + if(!match) { + app.missingInputIDs = missingInputIDs; + } + }); + } - res.json(filtered); + res.json(apps); }); }); diff --git a/ui/src/components/app.vue b/ui/src/components/app.vue index 33d1ae71..777d6d98 100644 --- a/ui/src/components/app.vue +++ b/ui/src/components/app.vue @@ -1,5 +1,10 @@ @@ -133,6 +146,20 @@ export default { if(this.app) this.app_ = this.app; }, + computed: { + + cardClasses() { + return { + 'clickable': this.clickable && this.isCompatible , + 'incompatible': this.isCompatible, + 'deprecated': this.app_.deprecated_by, + 'compact': this.compact, + } + // :class="{'compact': compact, 'clickable': clickable, 'deprecated': app_.deprecated_by}" + } + + }, + methods: { load_app() { this.appcache(this.appid, (err, app)=>{ @@ -147,6 +174,12 @@ export default { } }, + handleClick() { + if(this.isCompatible && this.clickable) { + this.click(); + } + } + }, } @@ -256,16 +289,50 @@ line-height: 100%; .deprecated h4 { opacity: 0.7; } + +.incompatible-label, .deprecated-label { -position: absolute; -right: 0; -top: 0; -background-color: #666; -color: white; -padding: 2px 4px; -opacity: 0.9; -text-transform: uppercase; -font-size: 80%; -font-weight: bold; + position: absolute; + right: 0; + background-color: #666; + color: white; + padding: 2px 4px; + opacity: 0.9; + text-transform: uppercase; + font-size: 80%; + font-weight: bold; + z-index: 1; +} + +.incompatible-error { + margin-left: 10px; + color: #d9534f; +} + +.deprecated-label { + top: 0; +} + +.incompatible-label { + top: 0px; } - + +.appcard.incompatible, +.appcard.deprecated { + pointer-events: none; + opacity: 0.5; +} + +.appcard.incompatible.name, +.appcard.deprecated.name, +.appcard.incompatible.github, +.appcard.deprecated.github { + color: #838383; +} + +.incompatible-label { + font-size: 70%; + /* //smaller to fit with icon of app */ +} + + \ No newline at end of file diff --git a/ui/src/components/datatypetag.vue b/ui/src/components/datatypetag.vue index 60942b86..49fb0927 100644 --- a/ui/src/components/datatypetag.vue +++ b/ui/src/components/datatypetag.vue @@ -1,5 +1,5 @@