Skip to content

Commit 5db9bca

Browse files
add a tool ontologies view as well
1 parent 0a2dd0a commit 5db9bca

8 files changed

Lines changed: 360 additions & 79 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<script setup lang="ts">
2+
import { faExternalLinkAlt, faFilter, faSort, faSortAlphaDown, faSortAlphaUp } from "@fortawesome/free-solid-svg-icons";
3+
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
4+
import { BAlert, BBadge, BDropdown, BDropdownItem, BFormInput } from "bootstrap-vue";
5+
import { computed, ref } from "vue";
6+
7+
import { useToolStore } from "@/stores/toolStore";
8+
import { errorMessageAsString } from "@/utils/simple-error";
9+
10+
import ToolOntologyCard from "./ToolOntologyCard.vue";
11+
import GButton from "@/components/BaseComponents/GButton.vue";
12+
import BreadcrumbHeading from "@/components/Common/BreadcrumbHeading.vue";
13+
import HelpText from "@/components/Help/HelpText.vue";
14+
import ScrollList from "@/components/ScrollList/ScrollList.vue";
15+
16+
const toolStore = useToolStore();
17+
18+
const ontologiesFilter = ref("");
19+
const loading = ref(true);
20+
const errorMessage = ref("");
21+
22+
const showing = ref<"operations" | "topics" | "all">("all");
23+
const sortOrder = ref<"asc" | "desc" | "indeterminate">("indeterminate");
24+
25+
const breadcrumbItems = computed(() => [
26+
{ title: "Discover Tools", to: "/tools/list" },
27+
{ title: "EDAM Ontologies", to: "/tools/list/ontologies" },
28+
]);
29+
30+
const edamOperations = computed(() => toolStore.getToolSections("ontology:edam_operations", ontologiesFilter.value));
31+
const edamTopics = computed(() => toolStore.getToolSections("ontology:edam_topics", ontologiesFilter.value));
32+
33+
const shownOntologies = computed(() => {
34+
let ontologies;
35+
switch (showing.value) {
36+
case "topics":
37+
ontologies = Object.values(edamTopics.value);
38+
break;
39+
case "operations":
40+
ontologies = Object.values(edamOperations.value);
41+
break;
42+
default:
43+
ontologies = Object.values(edamOperations.value).concat(Object.values(edamTopics.value));
44+
}
45+
if (sortOrder.value === "indeterminate") {
46+
return ontologies;
47+
}
48+
return [...ontologies].sort((a, b) => {
49+
const nameA = a.name?.toLowerCase() ?? "";
50+
const nameB = b.name?.toLowerCase() ?? "";
51+
if (nameA < nameB) {
52+
return sortOrder.value === "asc" ? -1 : 1;
53+
}
54+
if (nameA > nameB) {
55+
return sortOrder.value === "asc" ? 1 : -1;
56+
}
57+
return 0;
58+
});
59+
});
60+
61+
const ontologyDatalist = computed(() => {
62+
switch (showing.value) {
63+
case "topics":
64+
return toolStore.sectionDatalist("ontology:edam_topics");
65+
case "operations":
66+
return toolStore.sectionDatalist("ontology:edam_operations");
67+
default:
68+
return toolStore
69+
.sectionDatalist("ontology:edam_topics")
70+
.concat(toolStore.sectionDatalist("ontology:edam_operations"));
71+
}
72+
});
73+
74+
ensureOntologiesLoaded();
75+
76+
/** Ensures the two EDAM ontology tool panel views are in the store.
77+
*
78+
* _Note that the `toolStore` itself ensures a panel isn't loaded if it's already present in the state._
79+
*/
80+
async function ensureOntologiesLoaded() {
81+
try {
82+
await toolStore.fetchToolSections("ontology:edam_operations");
83+
await toolStore.fetchToolSections("ontology:edam_topics");
84+
} catch (error) {
85+
errorMessage.value = errorMessageAsString(error);
86+
} finally {
87+
loading.value = false;
88+
}
89+
}
90+
91+
function changeSort() {
92+
if (sortOrder.value === "indeterminate") {
93+
sortOrder.value = "asc";
94+
} else if (sortOrder.value === "asc") {
95+
sortOrder.value = "desc";
96+
} else {
97+
sortOrder.value = "indeterminate";
98+
}
99+
}
100+
</script>
101+
102+
<template>
103+
<div class="d-flex flex-column">
104+
<div class="d-flex flex-column flex-gapy-1 mb-2">
105+
<BreadcrumbHeading :items="breadcrumbItems" />
106+
107+
<BFormInput
108+
v-model="ontologiesFilter"
109+
type="text"
110+
placeholder="Filter ontologies"
111+
list="ontology-list-datalist" />
112+
<datalist v-if="ontologyDatalist.length" id="ontology-list-datalist">
113+
<option v-for="data in ontologyDatalist" :key="data.value" :label="data.text" :value="data.text" />
114+
</datalist>
115+
116+
<div class="d-flex justify-content-between align-items-center">
117+
<div class="d-flex flex-gapx-1 align-items-center">
118+
<HelpText uri="galaxy.tools.ontologies.description" text="What are EDAM Ontologies?" />
119+
120+
<GButton transparent href="https://edamontology.org/" target="_blank" inline icon-only>
121+
<FontAwesomeIcon :icon="faExternalLinkAlt" />
122+
</GButton>
123+
</div>
124+
125+
<div class="d-flex flex-gapx-1 align-items-center">
126+
<BBadge v-if="showing === 'topics'" class="edam-ontology-badge topic text-left" pill>
127+
<HelpText uri="galaxy.tools.ontologies.topic" text="What is an EDAM Topic?" />
128+
</BBadge>
129+
<BBadge v-else-if="showing === 'operations'" class="edam-ontology-badge operation text-left" pill>
130+
<HelpText uri="galaxy.tools.ontologies.operation" text="What is an EDAM Operation?" />
131+
</BBadge>
132+
133+
<BDropdown
134+
block
135+
:disabled="loading"
136+
variant="link"
137+
toggle-class="text-decoration-none"
138+
role="menu"
139+
aria-label="Choose whether to show operations, topics, or all"
140+
size="sm">
141+
<template v-slot:button-content>
142+
<span class="sr-only">Choose whether to show operations, topics, or all</span>
143+
<FontAwesomeIcon :icon="faFilter" />
144+
<i v-if="showing === 'all'">Showing all Ontologies</i>
145+
<span v-else-if="showing === 'topics'">Showing Topics Only</span>
146+
<span v-else>Showing Operations Only</span>
147+
</template>
148+
149+
<BDropdownItem @click="showing = 'all'">Show All Ontologies</BDropdownItem>
150+
<BDropdownItem @click="showing = 'operations'">Show Operations Only</BDropdownItem>
151+
<BDropdownItem @click="showing = 'topics'">Show Topics Only</BDropdownItem>
152+
</BDropdown>
153+
154+
<GButton color="blue" outline :pressed="sortOrder !== 'indeterminate'" @click="changeSort">
155+
<FontAwesomeIcon v-if="sortOrder === 'indeterminate'" :icon="faSort" />
156+
<FontAwesomeIcon v-else-if="sortOrder === 'asc'" :icon="faSortAlphaDown" />
157+
<FontAwesomeIcon v-else-if="sortOrder === 'desc'" :icon="faSortAlphaUp" />
158+
159+
Sort
160+
</GButton>
161+
</div>
162+
</div>
163+
</div>
164+
165+
<BAlert v-if="errorMessage" show variant="danger">
166+
{{ errorMessage }}
167+
</BAlert>
168+
169+
<div class="d-flex flex-column overflow-auto h-100">
170+
<ScrollList
171+
ref="root"
172+
:item-key="(tool) => tool.id"
173+
:prop-items="shownOntologies"
174+
:prop-total-count="shownOntologies.length"
175+
:prop-busy="loading"
176+
name="ontology"
177+
name-plural="ontologies"
178+
show-count-in-footer>
179+
<template v-slot:item="{ item }">
180+
<ToolOntologyCard :key="item.id" :ontology="item" routable />
181+
</template>
182+
</ScrollList>
183+
</div>
184+
</div>
185+
</template>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<script setup lang="ts">
2+
import { faExternalLinkAlt, faEye, faLink, faSitemap } from "@fortawesome/free-solid-svg-icons";
3+
import { computed } from "vue";
4+
import { useRouter } from "vue-router/composables";
5+
6+
import { getFullAppUrl } from "@/app/utils";
7+
import { Toast } from "@/composables/toast";
8+
import type { ToolSection } from "@/stores/toolStore";
9+
import { copy } from "@/utils/clipboard";
10+
11+
import type { CardAction, CardBadge, Title } from "../Common/GCard.types";
12+
13+
import GCard from "../Common/GCard.vue";
14+
15+
const router = useRouter();
16+
17+
const props = defineProps<{
18+
ontology: ToolSection;
19+
header?: boolean;
20+
routable?: boolean;
21+
}>();
22+
23+
const title = computed<Title>(() => {
24+
if (props.routable) {
25+
return {
26+
label: props.ontology.name,
27+
title: props.ontology.name,
28+
handler: () => {
29+
router.push(`/tools/list?ontology="${props.ontology.id}"`);
30+
},
31+
};
32+
} else {
33+
return props.ontology.name;
34+
}
35+
});
36+
37+
const badges = computed<CardBadge[]>(() => {
38+
return [
39+
{
40+
id: "ontology-id",
41+
label: props.ontology.id,
42+
title: "The EDAM id for this ontology",
43+
class: `edam-ontology-badge ${props.ontology.id.includes("operation") ? "operation" : "topic"}`,
44+
visible: true,
45+
icon: faSitemap,
46+
},
47+
];
48+
});
49+
50+
const primaryActions = computed<CardAction[]>(() => {
51+
const actions: CardAction[] = [];
52+
if (props.routable) {
53+
actions.push({
54+
id: "tools-list-ontology-view-link",
55+
label: "View Tools",
56+
icon: faEye,
57+
title: "View tools in this ontology",
58+
to: `/tools/list?ontology="${props.ontology.id}"`,
59+
});
60+
}
61+
return actions;
62+
});
63+
64+
const secondaryActions = computed<CardAction[]>(() => {
65+
const actions: CardAction[] = [];
66+
const ontologyId = props.ontology.id;
67+
const ontologyName = props.ontology.name;
68+
actions.push({
69+
id: "tools-list-ontology-filter-link",
70+
label: "Link to these results",
71+
icon: faLink,
72+
title: "Copy link to these results",
73+
variant: "outline-primary",
74+
handler: () => {
75+
const link = getFullAppUrl(`tools/list?ontology="${ontologyId}"`);
76+
copy(link);
77+
Toast.success(`Link to ontology "${ontologyName} (${ontologyId})" copied to clipboard`);
78+
},
79+
});
80+
if (props.ontology.links && "edam_browser" in props.ontology.links) {
81+
actions.push({
82+
id: "ontology-link",
83+
label: "EDAM Browser",
84+
icon: faExternalLinkAlt,
85+
title: "View in EDAM Browser",
86+
variant: "outline-primary",
87+
href: props.ontology.links.edam_browser,
88+
externalLink: true,
89+
});
90+
}
91+
return actions;
92+
});
93+
</script>
94+
95+
<template>
96+
<GCard
97+
v-if="props.ontology?.description"
98+
:current="props.header"
99+
:badges="badges"
100+
:primary-actions="primaryActions"
101+
:secondary-actions="secondaryActions"
102+
:description="props.ontology.description"
103+
full-description
104+
:title="title">
105+
<template v-slot:update-time>
106+
<i v-if="props.ontology.tools"> {{ props.ontology.tools.length }} tools in this ontology </i>
107+
</template>
108+
</GCard>
109+
</template>

client/src/components/ToolsList/ToolsList.vue

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { faStar } from "@fortawesome/free-solid-svg-icons";
2+
import { faSitemap, faStar } from "@fortawesome/free-solid-svg-icons";
33
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
44
import { storeToRefs } from "pinia";
55
import { computed, ref, watch } from "vue";
@@ -147,10 +147,22 @@ function applyFilter(filter: string, value: string) {
147147
<template>
148148
<section class="tools-list">
149149
<div class="mb-2">
150-
<div class="d-flex align-items-center justify-content-between">
150+
<div class="d-flex align-items-center justify-content-between flex-gapx-1">
151151
<Heading h1 separator inline size="lg" class="flex-grow-1 m-0">
152152
<span v-localize>Discover Tools in this Galaxy</span>
153153
</Heading>
154+
155+
<GButton
156+
size="small"
157+
outline
158+
tooltip
159+
tooltip-placement="bottom"
160+
color="blue"
161+
title="Discover Tool EDAM Ontologies"
162+
to="/tools/list/ontologies">
163+
<FontAwesomeIcon :icon="faSitemap" />
164+
Ontologies
165+
</GButton>
154166
</div>
155167

156168
<div class="d-flex flex-nowrap align-items-center flex-gapx-1 py-2">
@@ -254,17 +266,10 @@ function applyFilter(filter: string, value: string) {
254266
</template>
255267

256268
<style lang="scss" scoped>
257-
@import "theme/blue.scss";
258-
259269
.tools-list {
260270
display: flex;
261271
flex-flow: column;
262272
263-
:deep(.ontology-badge) {
264-
background-color: scale-color($brand-toggle, $lightness: +75%);
265-
border-color: scale-color($brand-toggle, $lightness: +55%);
266-
}
267-
268273
.tools-list-body {
269274
display: flex;
270275
flex-direction: column;

0 commit comments

Comments
 (0)