Merge pull request #20342 from dannon/tab-icons

[25.0] DatasetView and Card Polish
This commit is contained in:
Marius van den Beek
2025-06-10 21:10:32 +02:00
committed by GitHub
15 changed files with 309 additions and 55 deletions
+23
View File
@@ -1,3 +1,26 @@
import type { components } from "@/api";
import { GalaxyApi } from "@/api";
import { rethrowSimple } from "@/utils/simple-error";
export type CompositeFileInfo = components["schemas"]["CompositeFileInfo"];
export type DatatypeDetails = components["schemas"]["DatatypeDetails"];
/**
* Get details about a specific datatype
*/
export async function fetchDatatypeDetails(extension: string): Promise<DatatypeDetails> {
try {
const { GET } = GalaxyApi();
const { data } = await GET("/api/datatypes/{datatype}", {
params: {
path: { datatype: extension },
},
});
if (!data) {
throw new Error(`Failed to fetch datatype details for ${extension}`);
}
return data as DatatypeDetails;
} catch (error) {
rethrowSimple(error);
}
}
+5
View File
@@ -9301,6 +9301,11 @@ export interface components {
* @description The URL to a detailed description for this datatype
*/
description_url: string | null;
/**
* Display behavior
* @description How this datatype behaves when displayed with preview=True: 'inline' (can be displayed in browser) or 'download' (triggers download)
*/
display_behavior?: string | null;
/**
* Display in upload
* @description If True, the associated file extension will be displayed in the `File Format` select list in the `Upload File from your computer` tool in the `Get Data` tool section of the tool panel
+150 -45
View File
@@ -1,10 +1,13 @@
<script setup lang="ts">
import { BNav, BNavItem } from "bootstrap-vue";
import { faBug, faChartBar, faEye, faFileAlt, faInfoCircle, faPen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BLink, BNav, BNavItem } from "bootstrap-vue";
import { computed, ref, watch } from "vue";
import { usePersistentToggle } from "@/composables/persistentToggle";
import { useDatasetStore } from "@/stores/datasetStore";
import { useDatatypeVisualizationsStore } from "@/stores/datatypeVisualizationsStore";
import { useDatatypeStore } from "@/stores/datatypeStore";
import { bytesToString } from "@/utils/utils";
import DatasetError from "../DatasetInformation/DatasetError.vue";
import LoadingSpan from "../LoadingSpan.vue";
@@ -17,12 +20,12 @@ import VisualizationFrame from "@/components/Visualizations/VisualizationFrame.v
import CenterFrame from "@/entry/analysis/modules/CenterFrame.vue";
const datasetStore = useDatasetStore();
const datatypeVisualizationsStore = useDatatypeVisualizationsStore();
const datatypeStore = useDatatypeStore();
const { toggled: headerCollapsed, toggle: toggleHeaderCollapse } = usePersistentToggle("dataset-header-collapsed");
interface Props {
datasetId: string;
tab?: "details" | "edit" | "error" | "preview" | "visualize";
tab?: "details" | "edit" | "error" | "preview" | "raw" | "visualize";
}
const props = withDefaults(defineProps<Props>(), {
@@ -30,37 +33,40 @@ const props = withDefaults(defineProps<Props>(), {
});
const iframeLoading = ref(true);
const preferredVisualization = ref<string>();
const dataset = computed(() => datasetStore.getDataset(props.datasetId));
const headerState = computed(() => (headerCollapsed.value ? "closed" : "open"));
const isLoading = computed(() => datasetStore.isLoadingDataset(props.datasetId));
// Track datatype loading state
const isDatatypeLoading = ref(false);
// Consolidated loading state
const isLoading = computed(() => {
return datasetStore.isLoadingDataset(props.datasetId) || isDatatypeLoading.value;
});
const showError = computed(
() => dataset.value && (dataset.value.state === "error" || dataset.value.state === "failed_metadata")
);
const isAutoDownloadType = computed(
() => dataset.value && datatypeStore.isDatatypeAutoDownload(dataset.value.file_ext)
);
const preferredVisualization = computed(
() => dataset.value && datatypeStore.getPreferredVisualization(dataset.value.file_ext)
);
// Check if the dataset has a preferred visualization by datatype
async function checkPreferredVisualization() {
if (dataset.value && dataset.value.file_ext) {
try {
const mapping = await datatypeVisualizationsStore.getPreferredVisualizationForDatatype(
dataset.value.file_ext
);
if (mapping) {
preferredVisualization.value = mapping.visualization;
} else {
preferredVisualization.value = undefined;
}
} catch (error) {
preferredVisualization.value = undefined;
// Watch for changes to the dataset to fetch datatype info
watch(
() => dataset.value?.file_ext,
async () => {
if (dataset.value && dataset.value.file_ext) {
isDatatypeLoading.value = true;
await datatypeStore.fetchDatatypeDetails(dataset.value.file_ext);
isDatatypeLoading.value = false;
}
} else {
preferredVisualization.value = undefined;
}
}
// Watch for changes to the dataset to check for preferred visualizations
watch(() => dataset.value?.file_ext, checkPreferredVisualization, { immediate: true });
},
{ immediate: true }
);
</script>
<template>
@@ -76,7 +82,8 @@ watch(() => dataset.value?.file_ext, checkPreferredVisualization, { immediate: t
class="flex-grow-1"
:collapse="headerState"
@click="toggleHeaderCollapse">
{{ dataset?.hid }}: <span class="font-weight-bold">{{ dataset?.name }}</span>
<span class="dataset-hid">{{ dataset?.hid }}:</span>
<span class="dataset-name font-weight-bold">{{ dataset?.name }}</span>
<span class="dataset-state-header">
<DatasetState :dataset-id="datasetId" />
</span>
@@ -100,29 +107,52 @@ watch(() => dataset.value?.file_ext, checkPreferredVisualization, { immediate: t
{{ dataset.genome_build }}
</BLink>
</span>
<div v-if="dataset.misc_info" class="info">
<span class="value">{{ dataset.misc_info }}</span>
</div>
<span v-if="dataset.file_size" class="filesize">
<span v-localize class="prompt">size</span>
<span class="value font-weight-bold" v-html="bytesToString(dataset.file_size, false)" />
</span>
</div>
</transition>
</header>
<BNav pills class="my-2 p-2 bg-light border-bottom">
<BNavItem title="Preview" :active="tab === 'preview'" :to="`/datasets/${datasetId}/preview`">
Preview
<BNavItem
title="View a preview of the dataset contents"
:active="tab === 'preview'"
:to="`/datasets/${datasetId}/preview`">
<FontAwesomeIcon :icon="faEye" class="mr-1" /> Preview
</BNavItem>
<BNavItem
v-if="preferredVisualization"
title="View raw dataset contents"
:active="tab === 'raw'"
:to="`/datasets/${datasetId}/raw`">
<FontAwesomeIcon :icon="faFileAlt" class="mr-1" /> Raw
</BNavItem>
<BNavItem
v-if="!showError"
title="Visualize"
title="Explore available visualizations for this dataset"
:active="tab === 'visualize'"
:to="`/datasets/${datasetId}/visualize`">
Visualize
<FontAwesomeIcon :icon="faChartBar" class="mr-1" /> Visualize
</BNavItem>
<BNavItem title="Details" :active="tab === 'details'" :to="`/datasets/${datasetId}/details`">
Details
<BNavItem
title="View detailed information about this dataset"
:active="tab === 'details'"
:to="`/datasets/${datasetId}/details`">
<FontAwesomeIcon :icon="faInfoCircle" class="mr-1" /> Details
</BNavItem>
<BNavItem title="Edit" :active="tab === 'edit'" :to="`/datasets/${datasetId}/edit`">Edit</BNavItem>
<BNavItem v-if="showError" title="Error" :active="tab === 'error'" :to="`/datasets/${datasetId}/error`">
Error
<BNavItem
title="Edit dataset attributes and metadata"
:active="tab === 'edit'"
:to="`/datasets/${datasetId}/edit`">
<FontAwesomeIcon :icon="faPen" class="mr-1" /> Edit
</BNavItem>
<BNavItem
v-if="showError"
title="View error information for this dataset"
:active="tab === 'error'"
:to="`/datasets/${datasetId}/error`">
<FontAwesomeIcon :icon="faBug" class="mr-1" /> Error
</BNavItem>
</BNav>
<div v-if="tab === 'preview'" class="h-100">
@@ -131,22 +161,49 @@ watch(() => dataset.value?.file_ext, checkPreferredVisualization, { immediate: t
:dataset-id="datasetId"
:visualization="preferredVisualization"
@load="iframeLoading = false" />
<div v-else-if="isAutoDownloadType" class="auto-download-message p-4">
<div class="alert alert-info">
<h4>Download Required</h4>
<p>This file type ({{ dataset.file_ext }}) will download automatically when accessed directly.</p>
<p>File size: <strong v-html="bytesToString(dataset.file_size || 0, false)" /></p>
<a :href="`/datasets/${datasetId}/display`" class="btn btn-primary mt-2" download>
<FontAwesomeIcon :icon="faFileAlt" class="mr-1" /> Download File
</a>
</div>
</div>
<CenterFrame
v-else
:src="`/datasets/${datasetId}/display/?preview=True`"
:is_preview="true"
:is-preview="true"
@load="iframeLoading = false" />
</div>
<div v-else-if="tab === 'visualize'" class="d-flex flex-column overflow-hidden overflow-y">
<div v-else-if="tab === 'raw'" class="h-100">
<div v-if="isAutoDownloadType" class="auto-download-message p-4">
<div class="alert alert-info">
<h4>Download Required</h4>
<p>This file type ({{ dataset.file_ext }}) will download automatically when accessed directly.</p>
<p>File size: <strong v-html="bytesToString(dataset.file_size || 0, false)" /></p>
<a :href="`/datasets/${datasetId}/display`" class="btn btn-primary mt-2" download>
<FontAwesomeIcon :icon="faFileAlt" class="mr-1" /> Download File
</a>
</div>
</div>
<CenterFrame
v-else
:src="`/datasets/${datasetId}/display/?preview=True`"
:is-preview="true"
@load="iframeLoading = false" />
</div>
<div v-else-if="tab === 'visualize'" class="tab-content-panel">
<VisualizationsList :dataset-id="datasetId" />
</div>
<div v-else-if="tab === 'edit'" class="d-flex flex-column overflow-hidden overflow-y mt-2">
<div v-else-if="tab === 'edit'" class="tab-content-panel mt-2">
<DatasetAttributes :dataset-id="datasetId" />
</div>
<div v-else-if="tab === 'details'" class="d-flex flex-column overflow-hidden overflow-y mt-2">
<div v-else-if="tab === 'details'" class="tab-content-panel mt-2">
<DatasetDetails :dataset-id="datasetId" />
</div>
<div v-else-if="tab === 'error'" class="d-flex flex-column overflow-hidden overflow-y mt-2">
<div v-else-if="tab === 'error'" class="tab-content-panel mt-2">
<DatasetError :dataset-id="datasetId" />
</div>
</div>
@@ -161,6 +218,21 @@ watch(() => dataset.value?.file_ext, checkPreferredVisualization, { immediate: t
opacity: 1;
transition: all 0.25s ease;
overflow: hidden;
.datatype,
.dbkey,
.filesize {
margin-right: 1rem;
}
.prompt {
color: $text-muted;
margin-right: 0.25rem;
}
.blurb {
margin-bottom: 0.25rem;
}
}
.header-enter, /* change to header-enter-from with Vue 3 */
@@ -172,8 +244,41 @@ watch(() => dataset.value?.file_ext, checkPreferredVisualization, { immediate: t
opacity: 0;
}
.dataset-hid,
.dataset-state-header {
white-space: nowrap;
}
.dataset-hid {
margin-right: 0.25rem;
}
.dataset-name {
word-break: break-word;
}
.dataset-state-header {
font-size: $h5-font-size;
vertical-align: middle;
margin-left: 0.5rem;
}
.tab-content-panel {
display: flex;
flex-direction: column;
overflow: hidden;
overflow-y: auto;
}
.auto-download-message {
display: flex;
align-items: flex-start;
justify-content: center;
height: 100%;
.alert {
max-width: 600px;
width: 100%;
}
}
</style>
@@ -1,8 +1,10 @@
<script setup lang="ts">
import { faStop } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import axios from "axios";
import { BButton, BDropdown } from "bootstrap-vue";
import { faStop } from "font-awesome-6";
//@ts-ignore deprecated package without types (vue 2, remove this comment on vue 3 migration)
import { ScanEye } from "lucide-vue";
import { computed, type Ref, ref } from "vue";
import { getAppRoot } from "@/onload/loadConfig";
@@ -112,14 +114,14 @@ function onDisplay($event: MouseEvent) {
<BButton
v-if="isDataset"
v-b-tooltip.hover
title="Display"
title="View"
tabindex="0"
class="display-btn px-1"
size="sm"
variant="link"
:href="displayUrl"
@click.prevent.stop="onDisplay($event)">
<icon icon="eye" />
<ScanEye absolute-stroke-width :size="16" />
</BButton>
<BButton
v-if="writable && isHistoryItem"
@@ -39,12 +39,24 @@ const showDownloads = computed(() => {
const showError = computed(() => {
return props.item.state === "error" || props.item.state === "failed_metadata";
});
const showInfo = computed(() => {
return props.item.state !== "noPermission";
});
const showVisualizations = computed(() => {
return !props.item.purged && ["ok", "failed_metadata", "error"].includes(props.item.state);
});
const showRerun = computed(() => {
return props.item.accessible && props.item.rerunnable && props.item.creating_job && props.item.state != "upload";
});
const reportErrorUrl = computed(() => {
return prependPath(props.itemUrls.reportError!);
});
const showDetailsUrl = computed(() => {
return prependPath(props.itemUrls.showDetails!);
});
const visualizeUrl = computed(() => {
return prependPath(props.itemUrls.visualize!);
});
const rerunUrl = computed(() => {
return prependPath(props.itemUrls.rerun!);
});
@@ -69,6 +81,14 @@ function onError() {
window.location.href = reportErrorUrl.value;
}
function onInfo() {
router.push(`/datasets/${props.item.id}/details`);
}
function onVisualize() {
router.push(`/datasets/${props.item.id}/visualize`);
}
function onRerun() {
router.push(`/root?job_id=${props.item.creating_job}`);
}
@@ -103,6 +123,30 @@ function onRerun() {
<FontAwesomeIcon :icon="faLink" />
</BButton>
<BButton
v-if="showInfo"
v-b-tooltip.hover
class="info-btn px-1"
title="Dataset Details"
size="sm"
variant="link"
:href="showDetailsUrl"
@click.prevent.stop="onInfo">
<FontAwesomeIcon :icon="faInfoCircle" />
</BButton>
<BButton
v-if="showVisualizations"
v-b-tooltip.hover
class="visualize-btn px-1"
title="Visualize"
size="sm"
variant="link"
:href="visualizeUrl"
@click.prevent.stop="onVisualize">
<FontAwesomeIcon :icon="faChartBar" />
</BButton>
<BButton
v-if="showHighlight"
v-b-tooltip.hover
+29
View File
@@ -1,14 +1,28 @@
import UploadUtils from "components/Upload/utils";
import { defineStore } from "pinia";
import { fetchDatatypeDetails } from "@/api/datatypes";
export const useDatatypeStore = defineStore("datatypeStore", {
state: () => ({
uploadDatatypes: [],
datatypeDetails: {},
}),
getters: {
getUploadDatatypes: (state) => {
return state.uploadDatatypes;
},
getDatatypeDetails: (state) => (extension) => {
return state.datatypeDetails[extension];
},
isDatatypeAutoDownload: (state) => (extension) => {
const details = state.datatypeDetails[extension];
return details?.display_behavior === "download";
},
getPreferredVisualization: (state) => (extension) => {
const details = state.datatypeDetails[extension];
return details?.preferred_visualization?.visualization || null;
},
},
actions: {
async fetchUploadDatatypes() {
@@ -19,5 +33,20 @@ export const useDatatypeStore = defineStore("datatypeStore", {
console.log("Error: unable to load datatypes", err);
}
},
async fetchDatatypeDetails(extension) {
// Return cached details if available
if (this.datatypeDetails[extension]) {
return this.datatypeDetails[extension];
}
try {
const details = await fetchDatatypeDetails(extension);
this.datatypeDetails[extension] = details;
return details;
} catch (err) {
console.error(`Error: unable to load datatype details for ${extension}`, err);
return null;
}
},
},
});
+3 -3
View File
@@ -157,9 +157,9 @@ object_store_details:
dataset_view:
selectors:
_: .dataset-view
edit_tab: ".nav-item[title='Edit'] > a.nav-link"
details_tab: ".nav-item[title='Details'] > a.nav-link"
visualize_tab: ".nav-item[title='Visualize'] > a.nav-link"
edit_tab: ".nav-item[title='Edit dataset attributes and metadata'] > a.nav-link"
details_tab: ".nav-item[title='View detailed information about this dataset'] > a.nav-link"
visualize_tab: ".nav-item[title='Explore available visualizations for this dataset'] > a.nav-link"
history_panel:
menu:
+5
View File
@@ -66,6 +66,11 @@ class DatatypeDetails(BaseModel):
title="Upload warning",
description="End-user information regarding potential pitfalls with this upload type.",
)
display_behavior: Optional[str] = Field(
default=None,
title="Display behavior",
description="How this datatype behaves when displayed with preview=True: 'inline' (can be displayed in browser) or 'download' (triggers download)",
)
class DatatypesMap(BaseModel):
+3
View File
@@ -414,6 +414,7 @@ class CompressedZipArchive(CompressedArchive):
"""
file_ext = "zip"
display_behavior = "download" # Archive files trigger downloads
def set_peek(self, dataset: DatasetProtocol, **kwd) -> None:
if not dataset.dataset.purged:
@@ -3198,6 +3199,7 @@ class Xlsx(Binary):
file_ext = "xlsx"
compressed = True
display_behavior = "download" # Office documents trigger downloads
def sniff_prefix(self, file_prefix: FilePrefix) -> bool:
# Xlsx is compressed in zip format and must not be uncompressed in Galaxy.
@@ -3210,6 +3212,7 @@ class ExcelXls(Binary):
file_ext = "excel.xls"
edam_format = "format_3468"
display_behavior = "download" # Office documents trigger downloads
def sniff_prefix(self, file_prefix: FilePrefix) -> bool:
return file_prefix.mime_type == self.get_mime()
+21
View File
@@ -240,6 +240,10 @@ class Data(metaclass=DataMeta):
# A per datatype setting (inherited): max file size (in bytes) for setting optional metadata
_max_optional_metadata_filesize = None
# Display behavior when preview=True: "inline" (can be displayed in browser),
# "download" (always triggers download), or None (default behavior)
display_behavior: Optional[Literal["inline", "download"]] = None
# Trackster track type.
track_type: Optional[str] = None
@@ -264,6 +268,23 @@ class Data(metaclass=DataMeta):
return cls.allow_datatype_change
return cls.composite_type is None
@classmethod
def get_display_behavior(cls) -> str:
"""
Returns the display behavior for this datatype.
If display_behavior is set on the class, returns that value.
Otherwise, returns "inline" for text-based types and "download" for binary types.
"""
if cls.display_behavior is not None:
return cls.display_behavior
# Default behavior based on whether the datatype is binary
if cls.is_binary is True:
return "download"
else:
return "inline"
def dataset_content_needs_grooming(self, file_name: str) -> bool:
"""This function is called on an output dataset file after the content is initially generated."""
return False
+2
View File
@@ -228,6 +228,7 @@ class Png(Image):
class Tiff(Image):
edam_format = "format_3591"
file_ext = "tiff"
display_behavior = "download" # TIFF files trigger browser downloads
MetadataElement(
name="offsets",
desc="Offsets File",
@@ -505,6 +506,7 @@ class Rast(Image):
class Pdf(Image):
edam_format = "format_3508"
file_ext = "pdf"
display_behavior = "download" # PDFs often trigger downloads depending on browser settings
def sniff(self, filename: str) -> bool:
"""Determine if the file is in pdf format."""
+5
View File
@@ -343,6 +343,11 @@ class Registry:
"description": description,
"description_url": description_url,
"upload_warning": upload_warning(upload_warning_template),
"display_behavior": (
datatype_instance.get_display_behavior()
if hasattr(datatype_instance, "get_display_behavior")
else None
),
}
composite_files = datatype_instance.get_composite_files()
if composite_files:
+9 -3
View File
@@ -1995,7 +1995,9 @@ class NavigatesGalaxy(HasDriver):
# Find and click the Edit tab - using a more reliable selector
# BVue generates '.nav-item a' elements with a title attribute matching the tab title
edit_tab_button = self.wait_for_selector_clickable(".nav-item[title='Edit'] > a.nav-link")
edit_tab_button = self.wait_for_selector_clickable(
".nav-item[title='Edit dataset attributes and metadata'] > a.nav-link"
)
edit_tab_button.click()
# Wait for the edit attributes panel to be visible
@@ -2010,14 +2012,18 @@ class NavigatesGalaxy(HasDriver):
def show_dataset_details(self, hid):
self.display_dataset(hid)
# Find and click the Details tab
details_tab_button = self.wait_for_selector_clickable(".nav-item[title='Details'] > a.nav-link")
details_tab_button = self.wait_for_selector_clickable(
".nav-item[title='View detailed information about this dataset'] > a.nav-link"
)
details_tab_button.click()
self.components.dataset_details._.wait_for_visible()
def show_dataset_visualizations(self, hid):
self.display_dataset(hid)
# Find and click the Visualize tab
visualize_tab_button = self.wait_for_selector_clickable(".nav-item[title='Visualize'] > a.nav-link")
visualize_tab_button = self.wait_for_selector_clickable(
".nav-item[title='Explore available visualizations for this dataset'] > a.nav-link"
)
visualize_tab_button.click()
def history_panel_item_view_dataset_details(self, hid):
@@ -229,6 +229,9 @@ class FastAPIDatatypes:
"display_in_upload": datatype in self.datatypes_registry.upload_file_formats,
"mimetype": self.datatypes_registry.get_mimetype_by_extension(datatype),
"is_binary": getattr(dt_object, "is_binary", False),
"display_behavior": (
dt_object.get_display_behavior() if hasattr(dt_object, "get_display_behavior") else None
),
}
# Add composite files if applicable
+2 -1
View File
@@ -19,5 +19,6 @@
"dependencies": {
"@galaxyproject/galaxy-client": "^24.0.0",
"cpy-cli": "^5.0.0"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}