Skip to content

Commit

Permalink
Add WorkflowBox to Activity bar Side Panel
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmedhamidawan committed Apr 25, 2023
1 parent bc7d871 commit 35e1c38
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 7 deletions.
13 changes: 9 additions & 4 deletions client/src/components/ActivityBar/ActivityBar.vue
Expand Up @@ -3,6 +3,7 @@ import { useUserStore } from "@/stores/userStore";
import UploadItem from "./Items/UploadItem.vue";
import ToolBox from "@/components/Panels/ProviderAwareToolBox.vue";
import FlexPanel from "@/components/Panels/FlexPanel.vue";
import WorkflowBox from "@/components/Panels/WorkflowBox.vue";
import ActivityItem from "./ActivityItem";
const userStore = useUserStore();
Expand All @@ -25,14 +26,15 @@ function onToggleSidebar(toggle) {
icon="wrench"
title="Tools"
tooltip="Search and run tools"
:is-active="sidebarIsActive('search')"
@click="onToggleSidebar('search')" />
:is-active="sidebarIsActive('tools')"
@click="onToggleSidebar('tools')" />
<ActivityItem
id="activity-workflow"
title="Workflow"
icon="sitemap"
tooltip="Chain tools into workflows"
to="/workflows/list" />
:is-active="sidebarIsActive('workflows')"
@click="onToggleSidebar('workflows')" />
<ActivityItem
id="activity-visualization"
icon="chart-bar"
Expand All @@ -49,9 +51,12 @@ function onToggleSidebar(toggle) {
to="/user" />
</b-nav>
</div>
<FlexPanel v-if="sidebarIsActive('search')" key="search" side="left" :collapsible="false">
<FlexPanel v-if="sidebarIsActive('tools')" key="tools" side="left" :collapsible="false">
<ToolBox />
</FlexPanel>
<FlexPanel v-else-if="sidebarIsActive('workflows')" key="workflows" side="left" :collapsible="false">
<WorkflowBox />
</FlexPanel>
</div>
</template>

Expand Down
105 changes: 105 additions & 0 deletions client/src/components/Panels/WorkflowBox.vue
@@ -0,0 +1,105 @@
<script setup lang="ts">
import { getGalaxyInstance } from "@/app";
import { getAppRoot } from "@/onload";
import { useRouter } from "vue-router/composables";
import { ref, computed, onMounted, type ComputedRef } from "vue";
import { useWorkflowStore, type Workflow } from "@/stores/workflowStore";
import WorkflowSearch from "@/components/Workflow/WorkflowSearch.vue";
import FavoritesButton from "@/components/Panels/Buttons/FavoritesButton.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
const workflowStore = useWorkflowStore();
const router = useRouter();
const query = ref("");
const queryPending = ref(false);
const showAdvanced = ref(false);
const panelFilter: ComputedRef<any> = computed(() => {
return { name: query.value };
});
// on Mount, load all Workflows
onMounted(async () => {
queryPending.value = true;
await workflowStore.fetchWorkflows(panelFilter.value);
queryPending.value = false;
});
// computed
const workflows = computed(() => {
let results: Workflow[] = [];
if (query.value === "#favorites") {
const Galaxy = getGalaxyInstance();
results = Galaxy.config.stored_workflow_menu_entries;
} else if (!queryTooShort.value) {
results = workflowStore.getWorkflows(panelFilter.value);
}
return [
...results.map((wf) => {
return {
id: wf.id,
name: wf.name,
href: `${getAppRoot()}workflows/run?id=${wf.id}`,
};
}),
];
});
const queryTooShort: ComputedRef<boolean> = computed(() => query.value !== "" && query.value.length < 3);
// functions
async function onQuery(q: string) {
query.value = !q ? "" : q;
queryPending.value = true;
if (!queryTooShort.value && q !== "#favorites") {
await workflowStore.fetchWorkflows(panelFilter.value);
}
queryPending.value = false;
}
function onOpen(route: string) {
router.push(route);
}
</script>

<template>
<div class="unified-panel" aria-labelledby="workflowbox-heading">
<div unselectable="on">
<div class="unified-panel-header-inner">
<nav class="d-flex justify-content-between mx-3 my-2">
<h2 v-if="!showAdvanced" id="workflowbox-heading" v-localize class="m-1 h-sm">Workflows</h2>
<h2 v-else id="workflowbox-heading" v-localize class="m-1 h-sm">Advanced Workflow Search</h2>

<div class="panel-header-buttons">
<b-button-group>
<favorites-button v-if="!showAdvanced" :query="query" @onFavorites="onQuery" />
</b-button-group>
</div>
</nav>
</div>
</div>
<div class="unified-panel-controls">
<WorkflowSearch enable-advanced :show-advanced.sync="showAdvanced" :query="query" @onQuery="onQuery" />
<section v-if="!showAdvanced">
<div v-if="queryPending" class="pb-2">
<b-badge class="alert-info w-100"><LoadingSpan message="Loading workflows" /></b-badge>
</div>
<div v-else-if="queryTooShort" class="pb-2">
<b-badge class="alert-danger w-100">Search string too short!</b-badge>
</div>
<div v-else-if="workflows.length == 0" class="pb-2">
<b-badge class="alert-danger w-100">No results found!</b-badge>
</div>
</section>
</div>
<div v-if="!showAdvanced" class="unified-panel-body">
<div class="toolMenuContainer">
<div id="internal-workflows" class="toolSectionBody">
<div class="toolSectionBg" />
<div v-for="wf in workflows" :key="wf.id" class="toolTitle">
<a class="title-link" href="javascript:void(0)" @click="onOpen(wf.href)">{{ wf.name }}</a>
</div>
</div>
</div>
</div>
</div>
</template>
17 changes: 16 additions & 1 deletion client/src/components/Panels/utilities.js
@@ -1,11 +1,26 @@
/**
* Utilities file for Tool Search (panel/client search + advanced/backend search)
* Utilities file for Panel Searches (panel/client search + advanced/backend search)
*/
import { orderBy } from "lodash";

const TOOLS_RESULTS_SORT_LABEL = "apiSort";
const TOOLS_RESULTS_SECTIONS_HIDE = ["Expression Tools"];

// Converts filterSettings { key: value } to query = "key:value"
export function createWorkflowQuery(filterSettings) {
let query = "";
query = Object.entries(filterSettings)
.filter(([filter, value]) => value)
.map(([filter, value]) => {
return `${filter}:${value}`;
})
.join(" ");
if (Object.keys(filterSettings).length == 1 && filterSettings.name) {
return filterSettings.name;
}
return query;
}

// - Takes filterSettings = {"name": "Tool Name", "section": "Collection", ...}
// - Takes panelView (if not 'default', does ontology search at backend)
// - Takes toolbox (to find ontology id if given ontology name)
Expand Down
7 changes: 7 additions & 0 deletions client/src/components/Workflow/WorkflowList.vue
Expand Up @@ -169,6 +169,10 @@ export default {
type: String,
default: "success",
},
query: {
type: String,
required: false,
},
},
data() {
return {
Expand Down Expand Up @@ -219,6 +223,9 @@ export default {
created() {
this.root = getAppRoot();
this.services = new Services();
if (this.query) {
this.filter = this.query;
}
},
methods: {
async provider(ctx) {
Expand Down
86 changes: 86 additions & 0 deletions client/src/components/Workflow/WorkflowSearch.vue
@@ -0,0 +1,86 @@
<template>
<div>
<small v-if="showAdvanced">Filter by name:</small>
<DelayedInput
:class="!showAdvanced && 'mb-3'"
:query="query"
:delay="100"
:show-advanced="showAdvanced"
:enable-advanced="enableAdvanced"
:placeholder="showAdvanced ? 'any name' : placeholder"
@change="checkQuery"
@onToggle="onToggle" />
<div
v-if="showAdvanced"
description="advanced workflow filters"
@keyup.enter="onSearch"
@keyup.esc="onToggle(false)">
<small class="mt-1">Filter by tag:</small>
<b-form-input v-model="filterSettings['tag']" size="sm" placeholder="any tag" />
<div class="mt-3">
<b-button class="mr-1" size="sm" variant="primary" @click="onSearch">
<icon icon="search" />
<span>{{ "Search" | localize }}</span>
</b-button>
<b-button size="sm" @click="onToggle(false)">
<icon icon="redo" />
<span>{{ "Cancel" | localize }}</span>
</b-button>
</div>
</div>
</div>
</template>

<script>
import DelayedInput from "components/Common/DelayedInput";
import _l from "utils/localization";
import { createWorkflowQuery } from "components/Panels/utilities";
export default {
name: "WorkflowSearch",
components: {
DelayedInput,
},
props: {
enableAdvanced: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: _l("search workflows"),
},
query: {
type: String,
default: null,
},
showAdvanced: {
type: Boolean,
default: false,
},
},
data() {
return {
favorites: ["#favs", "#favorites", "#favourites"],
filterSettings: {},
};
},
methods: {
checkQuery(q) {
this.filterSettings["name"] = q;
if (this.favorites.includes(q)) {
this.$emit("onQuery", "#favorites");
} else {
this.$emit("onQuery", q);
}
},
onToggle(toggleAdvanced) {
this.$emit("update:show-advanced", toggleAdvanced);
},
onSearch() {
const query = { query: createWorkflowQuery(this.filterSettings) };
this.$router.push({ path: "/workflows/list", query: query });
},
},
};
</script>
1 change: 1 addition & 0 deletions client/src/entry/analysis/router.js
Expand Up @@ -460,6 +460,7 @@ export function getRouter(Galaxy) {
props: (route) => ({
importMessage: route.query["message"],
importStatus: route.query["status"],
query: route.query["query"],
}),
},
{
Expand Down
2 changes: 1 addition & 1 deletion client/src/stores/userStore.ts
Expand Up @@ -22,7 +22,7 @@ interface Preferences {
export const useUserStore = defineStore(
"userStore",
() => {
const toggledSideBar = ref("search");
const toggledSideBar = ref("tools");
const showActivityBar = ref(false);
const currentUser = ref<User | null>(null);
const currentPreferences = ref<Preferences | null>(null);
Expand Down
29 changes: 28 additions & 1 deletion client/src/stores/workflowStore.ts
@@ -1,15 +1,18 @@
import { defineStore } from "pinia";
import axios from "axios";
import type { Steps } from "@/stores/workflowStepStore";
import { createWorkflowQuery } from "@/components/Panels/utilities";
import { getAppRoot } from "@/onload/loadConfig";

interface Workflow {
export interface Workflow {
[index: string]: any;
steps: Steps;
}

export const useWorkflowStore = defineStore("workflowStore", {
state: () => ({
allWorkflows: [] as Workflow[],
workflowResults: [] as Workflow[],
workflowsByInstanceId: {} as { [index: string]: Workflow },
}),
getters: {
Expand All @@ -34,6 +37,15 @@ export const useWorkflowStore = defineStore("workflowStore", {
return storedWorkflow?.id;
};
},
getWorkflows: (state) => {
return (filterSettings: Record<string, string | boolean>) => {
if (Object.keys(filterSettings).length === 0) {
return state.allWorkflows;
} else {
return state.workflowResults;
}
};
},
},
actions: {
async fetchWorkflowForInstanceId(workflowId: string) {
Expand All @@ -44,5 +56,20 @@ export const useWorkflowStore = defineStore("workflowStore", {
state.workflowsByInstanceId[workflowId] = data as Workflow;
});
},
async fetchWorkflows(filterSettings: Record<string, string | boolean>) {
if (Object.keys(filterSettings).length !== 0) {
const query = createWorkflowQuery(filterSettings); // remove from here?
const { data } = await axios.get(`${getAppRoot()}api/workflows`, {
params: { search: query, skip_step_counts: true },
});
this.workflowResults = data;
} else if (this.allWorkflows.length === 0) {
// TODO: add all params: ?limit=50&offset=0&search=&skip_step_counts=true
const { data } = await axios.get(`${getAppRoot()}api/workflows`, {
params: { skip_step_counts: true },
});
this.allWorkflows = data;
}
},
},
});

0 comments on commit 35e1c38

Please sign in to comment.