From 0a402b6d8dd9b9e63dced9dfc5ee24b2aa3aaa36 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Thu, 27 Jun 2024 04:52:05 -0400 Subject: [PATCH] Latest prototype of a minimal workflow UI. --- .../components/ActivityBar/ActivityBar.vue | 16 ++++- .../components/Workflow/List/WorkflowCard.vue | 7 +- .../components/Workflow/List/WorkflowList.vue | 22 +++++- .../Workflow/List/WorkflowListActions.vue | 9 +++ .../src/entry/analysis/modules/Analysis.vue | 11 ++- client/src/entry/analysis/modules/Home.vue | 8 +++ .../analysis/modules/WorkflowLanding.vue | 16 +++++ client/src/stores/activitySetup.ts | 69 +++++++++++++++---- client/src/stores/activityStore.ts | 49 +++++++++++-- client/src/stores/userStore.ts | 7 ++ lib/galaxy/config/schemas/config_schema.yml | 28 ++++++++ lib/galaxy/managers/configuration.py | 2 + 12 files changed, 215 insertions(+), 29 deletions(-) create mode 100644 client/src/entry/analysis/modules/WorkflowLanding.vue diff --git a/client/src/components/ActivityBar/ActivityBar.vue b/client/src/components/ActivityBar/ActivityBar.vue index cde4d7b190ad..a1d580142005 100644 --- a/client/src/components/ActivityBar/ActivityBar.vue +++ b/client/src/components/ActivityBar/ActivityBar.vue @@ -132,11 +132,21 @@ function onToggleSidebar(toggle: string = "", to: string | null = null) { userStore.toggleSideBar(toggle); } +const syncActivities = () => { + activityStore.sync(); + if (config.value && ["workflow_centric", "workflow_runner"].indexOf(config.value.client_mode) >= 0) { + userStore.untoggleToolbarIfNeeded(); + } +}; + watch( () => hashedUserId.value, - () => { - activityStore.sync(); - } + syncActivities, +); + +watch( + isConfigLoaded, + syncActivities, ); diff --git a/client/src/components/Workflow/List/WorkflowCard.vue b/client/src/components/Workflow/List/WorkflowCard.vue index f217a1a76ec2..32cf53014eaa 100644 --- a/client/src/components/Workflow/List/WorkflowCard.vue +++ b/client/src/components/Workflow/List/WorkflowCard.vue @@ -27,11 +27,13 @@ interface Props { workflow: any; gridView?: boolean; publishedView?: boolean; + allowWorkflowManagement?: boolean; } const props = withDefaults(defineProps(), { gridView: false, publishedView: false, + allowWorkflowManagement: true, }); const emit = defineEmits<{ @@ -134,6 +136,7 @@ async function onTagClick(tag: string) { @@ -185,7 +188,7 @@ async function onTagClick(tag: string) {
[]; interface Props { activeList?: "my" | "shared_with_me" | "published"; + clientMode?: "full" | "workflow_centric" | "workflow_runner"; + initialFilterText?: string; } const props = withDefaults(defineProps(), { activeList: "my", + clientMode: "full", + initialFilterText: "", }); const router = useRouter(); @@ -85,6 +89,7 @@ const validFilters = computed(() => workflowFilters.value.getValidFilters(rawFil const invalidFilters = computed(() => workflowFilters.value.getValidFilters(rawFilters.value, true).invalidFilters); const isSurroundedByQuotes = computed(() => /^["'].*["']$/.test(filterText.value)); const hasInvalidFilters = computed(() => !isSurroundedByQuotes.value && Object.keys(invalidFilters.value).length > 0); +const allowWorkflowManagement = computed(() => props.clientMode == "full"); function updateFilterValue(filterKey: string, newValue: any) { const currentFilterText = filterText.value; @@ -194,6 +199,16 @@ watch([filterText, sortBy, sortDesc, showBookmarked], async () => { await load(true); }); +watch( + props, + () => { + if(props.initialFilterText && filterText.value == "") { + filterText.value = props.initialFilterText; + } + }, + { immediate: true }, +); + onMounted(() => { if (router.currentRoute.query.owner) { updateFilterValue("user", `'${router.currentRoute.query.owner}'`); @@ -208,10 +223,10 @@ onMounted(() => {
Workflows - +
- + My workflows @@ -247,7 +262,7 @@ onMounted(() => { - + @@ -10,6 +13,7 @@ + + diff --git a/client/src/stores/activitySetup.ts b/client/src/stores/activitySetup.ts index 17551fc89e2b..1ce4995cd3a4 100644 --- a/client/src/stores/activitySetup.ts +++ b/client/src/stores/activitySetup.ts @@ -1,22 +1,42 @@ /** * List of built-in activities */ -import { type Activity } from "@/stores/activityStore"; +import { type Activity, type ClientMode, type RawActivity } from "@/stores/activityStore"; import { type EventData } from "@/stores/eventStore"; -export const Activities = [ +function isWorkflowCentric(clientMode: ClientMode) : boolean { + return ["workflow_centric", "workflow_runner"].indexOf(clientMode) >= 0; +} + +function unlessWorkflowCentric(clientMode: ClientMode): boolean { + if (isWorkflowCentric(clientMode)) { + return false; + } else { + return true; + } +} + +function ifWorkflowCentric(clientMode: ClientMode): boolean { + if (isWorkflowCentric(clientMode)) { + return true; + } else { + return false; + } +} + +export const ActivitiesRaw: RawActivity[] = [ { anonymous: false, description: "Displays currently running interactive tools (ITs), if these are enabled by the administrator.", icon: "fa-laptop", id: "interactivetools", mutable: false, - optional: false, + optional: ifWorkflowCentric, panel: false, title: "Interactive Tools", tooltip: "Show active interactive tools", to: "/interactivetool_entry_points/list", - visible: true, + visible: unlessWorkflowCentric, }, { anonymous: true, @@ -24,12 +44,12 @@ export const Activities = [ icon: "upload", id: "upload", mutable: false, - optional: false, + optional: ifWorkflowCentric, panel: false, title: "Upload", to: null, tooltip: "Download from URL or upload files from disk", - visible: true, + visible: unlessWorkflowCentric, }, { anonymous: true, @@ -37,12 +57,12 @@ export const Activities = [ icon: "wrench", id: "tools", mutable: false, - optional: false, + optional: ifWorkflowCentric, panel: true, title: "Tools", - to: null, + to: "/tools", tooltip: "Search and run tools", - visible: true, + visible: unlessWorkflowCentric, }, { anonymous: true, @@ -81,7 +101,7 @@ export const Activities = [ title: "Visualization", to: null, tooltip: "Visualize datasets", - visible: true, + visible: unlessWorkflowCentric, }, { anonymous: true, @@ -94,7 +114,7 @@ export const Activities = [ title: "Histories", tooltip: "Show all histories", to: "/histories/list", - visible: true, + visible: unlessWorkflowCentric, }, { anonymous: false, @@ -107,7 +127,7 @@ export const Activities = [ title: "History Multiview", tooltip: "Select histories to show in History Multiview", to: "/histories/view_multiple", - visible: true, + visible: unlessWorkflowCentric, }, { anonymous: false, @@ -120,7 +140,7 @@ export const Activities = [ title: "Datasets", tooltip: "Show all datasets", to: "/datasets/list", - visible: true, + visible: unlessWorkflowCentric, }, { anonymous: true, @@ -133,7 +153,7 @@ export const Activities = [ title: "Pages", tooltip: "Show all pages", to: "/pages/list", - visible: true, + visible: unlessWorkflowCentric, }, { anonymous: false, @@ -146,10 +166,29 @@ export const Activities = [ title: "Libraries", tooltip: "Access data libraries", to: "/libraries", - visible: true, + visible: unlessWorkflowCentric, }, ]; +function resolveActivity(activity: RawActivity, clientMode: ClientMode) : Activity { + let optional = activity.optional; + let visible = activity.visible; + if (typeof optional === 'function') { + optional = optional(clientMode); + } + if (typeof visible === 'function') { + visible = visible(clientMode); + } + return { ...activity, optional, visible}; +} + +export function getActivities(clientMode: ClientMode) { + const resolve = (activity: RawActivity) => { + return resolveActivity(activity, clientMode); + } + return ActivitiesRaw.map(resolve); +} + export function convertDropData(data: EventData): Activity | null { if (data.history_content_type === "dataset") { return { diff --git a/client/src/stores/activityStore.ts b/client/src/stores/activityStore.ts index 377a68e08964..d38b0402e879 100644 --- a/client/src/stores/activityStore.ts +++ b/client/src/stores/activityStore.ts @@ -5,9 +5,10 @@ import { defineStore } from "pinia"; import { type Ref } from "vue"; +import { useConfig } from "@/composables/config"; import { useUserLocalStorage } from "@/composables/userLocalStorage"; -import { Activities } from "./activitySetup"; +import { ActivitiesRaw,getActivities } from "./activitySetup"; export interface Activity { // determine wether an anonymous user can access this activity @@ -34,6 +35,34 @@ export interface Activity { visible: boolean; } +export type ClientMode = "full" | "workflow_centric" | "workflow_runner"; + +// config materializes a RawActivity into an Activity +export interface RawActivity { + // determine wether an anonymous user can access this activity + anonymous: boolean; + // description of the activity + description: string; + // unique identifier + id: string; + // icon to be displayed in activity bar + icon: string; + // indicate if this activity can be modified and/or deleted + mutable: boolean; + // indicate wether this activity can be disabled by the user + optional: boolean | ((mode: ClientMode) => boolean); + // specifiy wether this activity utilizes the side panel + panel: boolean; + // title to be displayed in the activity bar + title: string; + // route to be executed upon selecting the activity + to: string | null; + // tooltip to be displayed when hovering above the icon + tooltip: string; + // indicate wether the activity should be visible by default + visible: boolean | ((mode: ClientMode) => boolean); +} + export const useActivityStore = defineStore("activityStore", () => { const activities: Ref> = useUserLocalStorage("activity-store-activities", []); @@ -41,7 +70,12 @@ export const useActivityStore = defineStore("activityStore", () => { * Restores the default activity bar items */ function restore() { - activities.value = Activities.slice(); + const { config, isConfigLoaded } = useConfig(); + if (!isConfigLoaded.value) { + return; + } + + activities.value = getActivities(config.value.client_mode); } /** @@ -50,11 +84,18 @@ export const useActivityStore = defineStore("activityStore", () => { * to the user stored activities which are persisted in local cache. */ function sync() { + const { config, isConfigLoaded } = useConfig(); + if (!isConfigLoaded.value) { + return; + } + // create a map of built-in activities const activitiesMap: Record = {}; - Activities.forEach((a) => { + const activityDefs = getActivities(config.value.client_mode); + activityDefs.forEach((a) => { activitiesMap[a.id] = a; }); + // create an updated array of activities const newActivities: Array = []; const foundActivity = new Set(); @@ -76,7 +117,7 @@ export const useActivityStore = defineStore("activityStore", () => { } }); // add new built-in activities - Activities.forEach((a) => { + activityDefs.forEach((a) => { if (!foundActivity.has(a.id)) { newActivities.push({ ...a }); } diff --git a/client/src/stores/userStore.ts b/client/src/stores/userStore.ts index 55a49d491209..9379bcb89634 100644 --- a/client/src/stores/userStore.ts +++ b/client/src/stores/userStore.ts @@ -146,6 +146,12 @@ export const useUserStore = defineStore("userStore", () => { }; } + function untoggleToolbarIfNeeded() { + if (toggledSideBar.value == "tools") { + toggledSideBar.value = ""; + } + } + return { currentUser, currentPreferences, @@ -163,6 +169,7 @@ export const useUserStore = defineStore("userStore", () => { addFavoriteTool, removeFavoriteTool, toggleSideBar, + untoggleToolbarIfNeeded, $reset, }; }); diff --git a/lib/galaxy/config/schemas/config_schema.yml b/lib/galaxy/config/schemas/config_schema.yml index afcb6ec9c3ed..80330a7e1fe2 100644 --- a/lib/galaxy/config/schemas/config_schema.yml +++ b/lib/galaxy/config/schemas/config_schema.yml @@ -3134,6 +3134,34 @@ mapping: When false, the most recently added compatible item in the history will be used for each "Set at Runtime" input, independent of others in the workflow. + client_mode: + type: str + default: 'full' + enum: ['full', 'workflow_centric', 'workflow_runner'] + required: false + per_host: true + desc: | + This will change the modality and focus on the client UI. The traditional + full Galaxy with default activity bar is the default of 'full'. + 'workflow_centric' & 'workflow_runner' yield client applications + that are geared to center a collection of workflows in Galaxy and attempts + to hide the concept of histories from users. The 'workflow_centric' view + still allows the user to manage & edit a collection of their own workflows. + 'workflow_runner' is a mode that disables workflow management to even further + simplify the UI - this may be appropriate for instances that really just want + enable particular workflows as-is. + + simplified_workflow_landing_initial_filter_text: + type: str + default: null + required: false + per_host: true + desc: | + If the Galaxy client is in 'workflow_centric' or 'workflow_runner' "client mode", + this controls the initial filtering of the workflow search textbox. This can + be used to foreground workflows in the published workflow list by tar (e.g. 'tag:XXX') + or username (e.g. 'username:XXXX'). + simplified_workflow_run_ui: type: str default: 'prefer' diff --git a/lib/galaxy/managers/configuration.py b/lib/galaxy/managers/configuration.py index d0ccfa8d53c2..8a6d30e8454d 100644 --- a/lib/galaxy/managers/configuration.py +++ b/lib/galaxy/managers/configuration.py @@ -164,6 +164,8 @@ def _config_is_truthy(item, key, **context): "enable_unique_workflow_defaults": _use_config, "enable_beta_markdown_export": _use_config, "enable_beacon_integration": _use_config, + "client_mode": _use_config, + "simplified_workflow_landing_initial_filter_text": _use_config, "simplified_workflow_run_ui": _use_config, "simplified_workflow_run_ui_target_history": _use_config, "simplified_workflow_run_ui_job_cache": _use_config,