Merge pull request #15628 from ElectronicBlueberry/training-recommendations

Add suggested Training material to Tool Form
This commit is contained in:
Dannon
2023-04-26 07:59:20 -04:00
committed by GitHub
9 changed files with 384 additions and 7 deletions
+8 -1
View File
@@ -11,6 +11,7 @@ import Heading from "components/Common/Heading";
import ToolSelectPreferredObjectStore from "./ToolSelectPreferredObjectStore";
import ToolTargetPreferredObjectStorePopover from "./ToolTargetPreferredObjectStorePopover";
import { getAppRoot } from "onload/loadConfig";
import ToolTutorialRecommendations from "./ToolTutorialRecommendations.vue";
import { computed, ref, watch } from "vue";
@@ -166,11 +167,17 @@ function onUpdatePreferredObjectStoreId(selectedToolPreferredObjectStoreId) {
<slot name="buttons" />
<div>
<div class="mt-2 mb-4">
<div v-if="props.options.help" class="mt-2 mb-4">
<Heading h2 separator bold size="sm"> Help </Heading>
<ToolHelp :content="props.options.help" />
</div>
<ToolTutorialRecommendations
:id="props.options.id"
:name="props.options.name"
:version="props.options.version"
:owner="props.options.tool_shed_repository?.owner" />
<ToolFooter
:id="props.id"
:has-citations="props.options.citations"
@@ -0,0 +1,83 @@
<script setup lang="ts">
import Heading from "@/components/Common/Heading.vue";
import { useToolTrainingMaterial } from "@/composables/toolTrainingMaterial";
import ExternalLink from "@/components/ExternalLink.vue";
import { BCollapse, BButton } from "bootstrap-vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faCaretDown } from "@fortawesome/free-solid-svg-icons";
import { useUid } from "@/composables/utils/uid";
import slugify from "slugify";
import { computed } from "vue";
const props = defineProps<{
name: string;
id: string;
version: string;
owner?: string;
}>();
//@ts-ignore: bad library types
library.add(faCaretDown);
const { trainingAvailable, trainingCategories, tutorialDetails, allTutorialsUrl, versionAvailable } =
useToolTrainingMaterial(props.id, props.name, props.version, props.owner);
const collapseId = useUid("collapse-");
function idForCategory(category: string) {
return `${collapseId.value}-${slugify(category)}`;
}
function tutorialsInCategory(category: string) {
return tutorialDetails.value.filter((tut) => tut.category === category);
}
const tutorialText = computed(() => {
if (tutorialDetails.value.length > 1) {
return `There are ${tutorialDetails.value.length} tutorials available which use this tool.`;
} else {
return "There is 1 tutorial available which uses this tool.";
}
});
</script>
<template>
<div v-if="trainingAvailable" class="mt-2 mb-4">
<Heading h2 separator bold size="sm">Tutorials</Heading>
<p>
{{ tutorialText }}
<span v-if="versionAvailable"> These tutorials include training for the current version of the tool. </span>
<ExternalLink v-if="allTutorialsUrl" :href="allTutorialsUrl">
View all tutorials referencing this tool.
</ExternalLink>
</p>
<BButton v-b-toggle="collapseId" class="ui-link">
<b>
Tutorials available in {{ trainingCategories.length }}
{{ trainingCategories.length > 1 ? "categories" : "category" }}
</b>
<FontAwesomeIcon icon="caret-down" />
</BButton>
<BCollapse :id="collapseId">
<div v-for="category in trainingCategories" :key="category">
<BButton v-b-toggle="idForCategory(category)" class="ui-link ml-3">
{{ category }} ({{ tutorialsInCategory(category).length }})
<FontAwesomeIcon icon="caret-down" />
</BButton>
<BCollapse :id="idForCategory(category)">
<ul class="d-flex flex-column my-1">
<li v-for="tutorial in tutorialsInCategory(category)" :key="tutorial.title">
<ExternalLink :href="tutorial.url.toString()" class="ml-2">
{{ tutorial.title }}
</ExternalLink>
</li>
</ul>
</BCollapse>
</div>
</BCollapse>
</div>
</template>
@@ -0,0 +1,192 @@
import { useConfig } from "./config";
import { computed, ref, watch, type Ref } from "vue";
import { escapeRegExp } from "@/utils/regExp";
type TrainingDetails = {
tool_id: Array<
[
string, // toolshed tool ID
string // Version
]
>;
tutorials: Array<
[
string, // tutorial ID (unused)
string, // Title
string, // Category
string // URL
]
>;
};
type TrainingMaterialResponse = {
[id: string]: TrainingDetails;
};
type Config = {
tool_training_recommendations: boolean;
tool_training_recommendations_api_url: string;
tool_training_recommendations_link: string;
};
export type TutorialDetails = {
category: string;
title: string;
url: URL;
};
/** caches the response of the training material api */
const cachedResponse: Ref<TrainingMaterialResponse | null> = ref(null);
/** maps toolshed tool ids to training tool ids */
const toolIdMap: Map<string, string> = new Map();
function mapToolIds() {
Object.entries(cachedResponse.value ?? {}).forEach(([trainingId, details]) => {
details.tool_id.forEach(([id, version]) => {
if (id === version) {
// built-in tool
toolIdMap.set(id, trainingId);
} else {
const regEx = new RegExp(`${escapeRegExp(version)}$`);
const trimmedId = id.replace(regEx, "");
toolIdMap.set(trimmedId, trainingId);
}
});
});
}
/** Training information about given tool */
export function useToolTrainingMaterial(id: string, name: string, version: string, owner?: string) {
const { config, isLoaded }: { config: Ref<Config>; isLoaded: Ref<boolean> } = useConfig();
const apiEnabled = computed(() => {
return Boolean(
isLoaded.value &&
config.value.tool_training_recommendations &&
config.value.tool_training_recommendations_api_url
);
});
const cacheLoaded = ref(false);
watch(
() => isLoaded.value,
async () => {
if (!isLoaded.value) {
return;
}
if (apiEnabled.value && !cachedResponse.value) {
const res = await fetch(config.value.tool_training_recommendations_api_url);
if (res.ok) {
cachedResponse.value = await res.json();
mapToolIds();
}
}
cacheLoaded.value = true;
},
{ immediate: true }
);
const identifier = computed(() => {
const regEx = new RegExp(`${escapeRegExp(version)}$`);
const trimmedId = id.replace(regEx, "");
if (!cacheLoaded.value) {
return trimmedId;
} else {
return toolIdMap.get(trimmedId) ?? trimmedId;
}
});
const trainingAvailable = computed(() => {
if (!apiEnabled.value || !cachedResponse.value) {
return false;
}
return Object.keys(cachedResponse.value).includes(identifier.value);
});
const trainingDetails = computed(() => {
if (!trainingAvailable.value) {
return null;
}
return cachedResponse.value?.[identifier.value] ?? null;
});
const trainingCategories = computed<string[]>(() => {
if (!trainingDetails.value) {
return [];
}
const categories = new Set<string>();
trainingDetails.value.tutorials.forEach((tutorial) => {
categories.add(tutorial[2]);
});
return Array.from(categories);
});
const tutorialDetails = computed<TutorialDetails[]>(() => {
if (!trainingDetails.value) {
return [];
}
const details: TutorialDetails[] = [];
trainingDetails.value.tutorials.forEach((tutorial) => {
details.push({
title: tutorial[1],
category: tutorial[2],
url: new URL(tutorial[3], config.value.tool_training_recommendations_api_url),
});
});
return details;
});
const allTutorialsUrl = computed<string | undefined>(() => {
if (!cacheLoaded.value || !config.value.tool_training_recommendations_link) {
return;
}
let url = config.value.tool_training_recommendations_link;
url = url.replace("{training_tool_identifier}", identifier.value);
url = url.replace("{tool_id}", id);
url = url.replace("{name}", name);
url = url.replace("{repository_owner}", owner ?? "");
url = url.replace("{version}", version);
return url;
});
const versionAvailable = computed<boolean>(() => {
if (!trainingDetails.value) {
return false;
}
for (let i = 0; i < trainingDetails.value.tool_id.length; i++) {
const element = trainingDetails.value.tool_id[i]!;
if (element[1] === version) {
return true;
}
}
return false;
});
return {
trainingAvailable,
trainingCategories,
tutorialDetails,
allTutorialsUrl,
versionAvailable,
};
}
+5
View File
@@ -392,6 +392,10 @@ $ui-margin-horizontal-large: $margin-v * 2;
column-gap: 0.25rem;
}
.flex-gapy-1 {
row-gap: 0.25rem;
}
/* Heading Sizes */
.h-xl {
font-size: $h1-font-size;
@@ -422,6 +426,7 @@ $ui-margin-horizontal-large: $margin-v * 2;
display: inline;
line-height: unset;
vertical-align: unset;
user-select: text;
&:hover {
text-decoration: underline;
+10
View File
@@ -0,0 +1,10 @@
/**
* Escapes all RegExp control characters from a string, so it can be matched literally
* @param string input string
* @returns string with all control characters escaped
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
*/
export function escapeRegExp(string: string) {
return string.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
+33
View File
@@ -5092,4 +5092,37 @@
:Type: bool
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``tool_training_recommendations``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Description:
Displays a link to training material, if any includes the current
tool. When activated the following options also need to be set:
tool_training_recommendations_link,
tool_training_recommendations_api_url
:Default: ``true``
:Type: bool
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``tool_training_recommendations_link``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Description:
Template URL to display all tutorials containing current tool.
Valid template inputs are: {repository_owner} {name}
{tool_id} {training_tool_identifier} {version}
:Default: ``https://training.galaxyproject.org/training-material/by-tool/{training_tool_identifier}.html``
:Type: str
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``tool_training_recommendations_api_url``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Description:
URL to API describing tutorials containing specific tools. When
CORS is used, make sure to add this host.
:Default: ``https://training.galaxyproject.org/training-material/api/top-tools.json``
:Type: str
+20 -6
View File
@@ -1,21 +1,21 @@
# Galaxy is configured by default to be usable in a single-user development
# environment. To tune the application for a multi-user production
# environment, see the documentation at:
#
#
# https://docs.galaxyproject.org/en/master/admin/production.html
#
#
# Throughout this sample configuration file, except where stated otherwise,
# uncommented values override the default if left unset, whereas commented
# values are set to the default value. Relative paths are relative to the root
# Galaxy directory.
#
#
# Examples of many of these options are explained in more detail in the Galaxy
# Community Hub.
#
#
# https://galaxyproject.org/admin/config
#
#
# Config hackers are encouraged to check there before asking for help.
#
#
# Configuration for Gravity process manager.
# ``uwsgi:`` section will be ignored if Galaxy is started via Gravity commands (e.g ``./run.sh``, ``galaxy`` or ``galaxyctl``).
gravity:
@@ -2715,3 +2715,17 @@ galaxy:
# integration.
#enable_beacon_integration: false
# Displays a link to training material, if any includes the current
# tool. When activated the following options also need to be set:
# tool_training_recommendations_link,
# tool_training_recommendations_api_url
#tool_training_recommendations: true
# Template URL to display all tutorials containing current tool. Valid
# template inputs are: {repository_owner} {name} {tool_id}
# {training_tool_identifier} {version}
#tool_training_recommendations_link: https://training.galaxyproject.org/training-material/by-tool/{training_tool_identifier}.html
# URL to API describing tutorials containing specific tools. When CORS
# is used, make sure to add this host.
#tool_training_recommendations_api_url: https://training.galaxyproject.org/training-material/api/top-tools.json
@@ -3705,3 +3705,33 @@ mapping:
desc: |
Enables user preferences and api endpoint for the beacon integration.
tool_training_recommendations:
type: bool
default: true
required: false
desc: |
Displays a link to training material, if any includes the current tool.
When activated the following options also need to be set:
tool_training_recommendations_link,
tool_training_recommendations_api_url
tool_training_recommendations_link:
type: str
default: https://training.galaxyproject.org/training-material/by-tool/{training_tool_identifier}.html
required: false
desc: |
Template URL to display all tutorials containing current tool.
Valid template inputs are:
{repository_owner}
{name}
{tool_id}
{training_tool_identifier}
{version}
tool_training_recommendations_api_url:
type: str
default: https://training.galaxyproject.org/training-material/api/top-tools.json
required: false
desc: |
URL to API describing tutorials containing specific tools.
When CORS is used, make sure to add this host.
+3
View File
@@ -203,6 +203,9 @@ class ConfigSerializer(base.ModelSerializer):
"user_library_import_dir_available": lambda item, key, **context: bool(item.get("user_library_import_dir")),
"welcome_directory": _use_config,
"themes": _use_config,
"tool_training_recommendations": _use_config,
"tool_training_recommendations_link": _use_config,
"tool_training_recommendations_api_url": _use_config,
}