Skip to content

Commit

Permalink
Merge pull request #638 from dannon/workflow-list-updates
Browse files Browse the repository at this point in the history
Website updates from hackathon
  • Loading branch information
mvdbeek authored Feb 27, 2025
2 parents 1562378 + 637f53b commit 9c2c36b
Show file tree
Hide file tree
Showing 18 changed files with 8,249 additions and 7,510 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
marko~=1.0
packaging~=21.0
planemo>=0.74.5
requests~=2.32
191 changes: 128 additions & 63 deletions scripts/workflow_manifest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os
import json
import yaml
import datetime
import requests
from urllib.parse import quote_plus
from create_mermaid import walk_directory


Expand All @@ -11,9 +14,56 @@ def read_contents(path: str):
except FileNotFoundError:
print(f"No {os.path.basename(path)} at {path}")
except Exception as e:
print(
f"Error reading file {path}: {e}"
)
print(f"Error reading file {path}: {e}")


def get_dockstore_details(trsID):
hash_stripped = trsID.replace("#workflow/", "", 1)
encoded_id = quote_plus(hash_stripped)

# Query the top-level details of the workflow
url_details = f"https://dockstore.org/api/workflows/path/workflow/{encoded_id}/published?include=validations%2Cauthors%2Cmetrics&subclass=BIOWORKFLOW&versionName=main"
response = requests.get(url_details)

details = None
categories = []
collections = []

if response.status_code == 200:
details = response.json()

entry_id = details.get("id")
if entry_id:
# With the ID, request categories
url_categories = f"https://dockstore.org/api/entries/{entry_id}/categories"
cat_response = requests.get(url_categories)

if cat_response.status_code == 200:
categories_data = cat_response.json()
for category in categories_data:
categories.append(category["displayName"])
else:
print(
f"Failed to get categories. Status code: {cat_response.status_code}"
)

# With the ID, request collections
url_collections = f"https://dockstore.org/api/entries/{entry_id}/collections"
collection_response = requests.get(url_collections)

if collection_response.status_code == 200:
collections_data = collection_response.json()
for collection in collections_data:
collections.append(collection["collectionDisplayName"])
else:
print(
f"Failed to get collections. Status code: {collection_response.status_code}"
)
else:
print("No 'id' field found in the top-level data.")
else:
print(f"Failed to retrieve details. Status code: {response.status_code}")
return details, categories, collections


def find_and_load_compliant_workflows(directory):
Expand All @@ -26,67 +76,82 @@ def find_and_load_compliant_workflows(directory):
workflow_data = []
for root, _, files in os.walk(directory):
if ".dockstore.yml" in files:
try:
dockstore_path = os.path.join(root, ".dockstore.yml")
with open(dockstore_path) as f:
workflow_details = yaml.safe_load(f)
workflow_details["path"] = root
workflow_data.append(workflow_details)

# Now inspect the details which are something like this:
# version: 1.2
# workflows:
# - name: Velocyto-on10X-from-bundled
# subclass: Galaxy
# publish: true
# primaryDescriptorPath: /Velocyto-on10X-from-bundled.ga
# testParameterFiles:
# - /Velocyto-on10X-from-bundled-tests.yml
# authors:
# - name: Lucille Delisle
# orcid: 0000-0002-1964-4960
# - name: Velocyto-on10X-filtered-barcodes
# subclass: Galaxy
# publish: true
# primaryDescriptorPath: /Velocyto-on10X-filtered-barcodes.ga
# testParameterFiles:
# - /Velocyto-on10X-filtered-barcodes-tests.yml
# authors:
# - name: Lucille Delisle
# orcid: 0000-0002-1964-4960

for workflow in workflow_details["workflows"]:
# For each listed workflow, load the primaryDescriptorPath
# file, which is the actual galaxy workflow.
# strip leading slash from primaryDescriptorPath if present -- these are relative.
workflow_path = os.path.join(
root, workflow["primaryDescriptorPath"].lstrip("/")
dockstore_path = os.path.join(root, ".dockstore.yml")
with open(dockstore_path) as f:
workflow_details = yaml.safe_load(f)
workflow_details["path"] = root
workflow_data.append(workflow_details)

# Now inspect the details which are something like this:
# version: 1.2
# workflows:
# - name: Velocyto-on10X-from-bundled
# subclass: Galaxy
# publish: true
# primaryDescriptorPath: /Velocyto-on10X-from-bundled.ga
# testParameterFiles:
# - /Velocyto-on10X-from-bundled-tests.yml
# authors:
# - name: Lucille Delisle
# orcid: 0000-0002-1964-4960
# - name: Velocyto-on10X-filtered-barcodes
# subclass: Galaxy
# publish: true
# primaryDescriptorPath: /Velocyto-on10X-filtered-barcodes.ga
# testParameterFiles:
# - /Velocyto-on10X-filtered-barcodes-tests.yml
# authors:
# - name: Lucille Delisle
# orcid: 0000-0002-1964-4960

for workflow in workflow_details["workflows"]:
# For each listed workflow, load the primaryDescriptorPath
# file, which is the actual galaxy workflow.
# strip leading slash from primaryDescriptorPath if present -- these are relative.
workflow_path = os.path.join(
root, workflow["primaryDescriptorPath"].lstrip("/")
)
try:
with open(workflow_path) as f:
workflow["definition"] = json.load(f)
except Exception as e:
print(
f"No workflow file: {os.path.join(root, workflow['primaryDescriptorPath'])}: {e}"
)
try:
with open(workflow_path) as f:
workflow["definition"] = json.load(f)
except Exception as e:
print(
f"No workflow file: {os.path.join(root, workflow['primaryDescriptorPath'])}: {e}"
)

# load readme, changelog and diagrams
workflow["readme"] = read_contents(os.path.join(root, "README.md"))
workflow["changelog"] = read_contents(os.path.join(root, "CHANGELOG.md"))
workflow["diagrams"] = read_contents(f"{os.path.splitext(workflow_path)[0]}_diagrams.md")
dirname = os.path.dirname(workflow_path).split("/")[-1]
workflow["trsID"] = f"#workflow/github.com/iwc-workflows/{dirname}/{workflow['name'] or 'main'}"

workflow_test_path = f"{workflow_path.rsplit('.ga', 1)[0]}-tests.yml"
if os.path.exists(workflow_test_path):
with open(workflow_test_path) as f:
tests = yaml.safe_load(f)
workflow["tests"] = tests
else:
print(f"no test for {workflow_test_path}")

except Exception as e:
print(f"Error reading file {os.path.join(root, '.dockstore.yml')}: {e}")

# Get workflow file update time and add it to the data as
# isoformat -- most accurate version of the latest 'update'
# to the workflow?
updated_timestamp = os.path.getmtime(workflow_path)
updated_datetime = datetime.datetime.fromtimestamp(updated_timestamp)
updated_isoformat = updated_datetime.isoformat()
workflow["updated"] = updated_isoformat

# load readme, changelog and diagrams
workflow["readme"] = read_contents(os.path.join(root, "README.md"))
workflow["changelog"] = read_contents(
os.path.join(root, "CHANGELOG.md")
)
workflow["diagrams"] = read_contents(
f"{os.path.splitext(workflow_path)[0]}_diagrams.md"
)
dirname = os.path.dirname(workflow_path).split("/")[-1]
trsID = f"#workflow/github.com/iwc-workflows/{dirname}/{workflow['name'] or 'main'}"
workflow["trsID"] = trsID

dockstore_details, categories, collections = get_dockstore_details(trsID)

workflow["dockstore_id"] = dockstore_details["id"]
workflow["categories"] = categories
workflow["collections"] = collections

workflow_test_path = f"{workflow_path.rsplit('.ga', 1)[0]}-tests.yml"
if os.path.exists(workflow_test_path):
with open(workflow_test_path) as f:
tests = yaml.safe_load(f)
workflow["tests"] = tests
else:
print(f"no test for {workflow_test_path}")

return workflow_data

Expand Down
2 changes: 1 addition & 1 deletion website/app.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="h-dvh flex flex-col">
<div class="h-dvh flex flex-col bg-chicago-50">
<IWCHeader />
<main class="flex overflow-hidden">
<NuxtPage />
Expand Down
79 changes: 79 additions & 0 deletions website/components/Filters.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script setup lang="ts">
import { useWorkflowStore } from "~/stores/workflows";
const store = useWorkflowStore();
const validFilters = computed(() => store.validFilters);
const invalidFilters = computed(() => store.invalidFilters);
const allFilters = computed(() => store.allFilters);
const handleFilterClick = (filter: string) => {
store.toggleFilter(filter);
};
</script>

<template>
<div id="filters" class="mb-4">
<TransitionGroup name="filter-transition">
<UBadge
v-for="filter in allFilters"
:key="filter"
:variant="store.selectedFilters.includes(filter) ? 'solid' : 'soft'"
:class="['badge m-1', { incompatible: !validFilters.includes(filter) }]"
@click="validFilters.includes(filter) ? handleFilterClick(filter) : null"
:data-tooltip="!validFilters.includes(filter) ? 'Incompatible with current selection' : null">
{{ filter }}
</UBadge>
</TransitionGroup>
</div>
</template>

<style scoped>
.incompatible {
opacity: 0.65;
cursor: not-allowed;
background-color: transparent !important;
box-shadow: none !important;
transition: all 0.3s ease; /* Add transition for a nice visual effect */
}
.incompatible:hover {
opacity: 0.85; /* Slightly increase opacity on hover for better feedback */
}
.filter-transition-move,
.filter-transition-enter-active,
.filter-transition-leave-active {
transition: all 0.5s ease;
}
.filter-transition-enter-from,
.filter-transition-leave-to {
opacity: 0;
transform: translateY(20px);
}
.filter-transition-leave-active {
position: absolute;
}
/* Custom tooltip styles */
[data-tooltip] {
position: relative;
}
[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 0.25rem 0.5rem;
background-color: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 0.25rem;
font-size: 0.75rem;
white-space: nowrap;
z-index: 10;
margin-bottom: 0.25rem;
}
</style>
9 changes: 5 additions & 4 deletions website/components/IWCHeader.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<template>
<header class="p-8 bg-ebony-clay-800 border-b text-white">
<header class="py-2 px-6 bg-ebony-clay text-white">
<div class="flex">
<div class="flex flex-grow space-x-2">
<NuxtLink to="/" class="flex items-center space-x-2 hover:text-hokey-pokey">
<img src="/iwc_logo_white.png" alt="IWC Logo" width="128" height="128" />
<span class="text-4xl font-mono font-semibold px-4"> Galaxy IWC - Workflow Library </span>
<img src="/iwc_logo_white.png" alt="IWC Logo" width="64" height="64" />
<span class="text-xl font-semibold px-4"> Galaxy IWC - Workflow Library </span>
</NuxtLink>
</div>
<div class="flex items-center space-x-8 font-mono">
<div class="flex items-center space-x-8">
<NuxtLink to="/about" class="hover:text-hokey-pokey"> About </NuxtLink>
<NuxtLink
to="https://github.com/galaxyproject/iwc/blob/main/workflows/README.md#adding-workflows"
class="hover:text-hokey-pokey">
Expand Down
27 changes: 27 additions & 0 deletions website/components/PopularWorkflows.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup lang="ts">
import { useWorkflowStore } from "~/stores/workflows";
// TODO: Could use any other identifier instead of trsId that seems fit
const props = defineProps<{
/** List of Trs Ids for most popular workflows */
workflowTrsIds: string[];
}>();
const workflowStore = useWorkflowStore();
const workflows = computed(() => {
return props.workflowTrsIds?.map((trsID) => workflowStore.getWorkflowByTrsId(trsID));
});
</script>

<template>
<div class="w-full max-w-6xl mx-auto">
<h2 class="text-xl my-6 text-white text-center font-semibold">
Get started with one of our most popular workflows, or browse the full library below.
</h2>
<div class="grid grid-cols-3 gap-4 mx-auto px-4">
<WorkflowCard v-for="workflow in workflows" :key="workflow.definition.uuid" :workflow="workflow" compact />
</div>
</div>
</template>
Loading

0 comments on commit 9c2c36b

Please sign in to comment.