Merge pull request #20290 from guerler/adjust_dataset_view

[25.0] Touch up Dataset View
This commit is contained in:
Dannon
2025-05-20 15:44:24 -04:00
committed by GitHub
11 changed files with 190 additions and 412 deletions
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed } from "vue";
import { STATES } from "@/components/History/Content/model/states";
import { useDatasetStore } from "@/stores/datasetStore";
const datasetStore = useDatasetStore();
const props = defineProps<{
datasetId: string;
}>();
const dataset = computed(() => datasetStore.getDataset(props.datasetId));
const contentState = computed(() => {
const state = dataset.value && dataset.value.state;
return state && STATES[state] ? STATES[state] : null;
});
const contentCls = computed(() => {
const status = contentState.value && contentState.value.status;
if (!status) {
return `alert-success`;
} else {
return `alert-${status}`;
}
});
</script>
<template>
<span v-if="dataset && contentState" class="rounded px-2 py-1 ml-2" :class="contentCls">
<span v-if="contentState.icon" class="mr-1">
<FontAwesomeIcon fixed-width :icon="contentState.icon" :spin="contentState.spin" />
</span>
{{ contentState.text || dataset.state || "n/a" }}
</span>
</template>
@@ -226,13 +226,6 @@ describe("DatasetView", () => {
expect(wrapper.props().tab).toBe("edit");
});
it("redirects invalid tabs to preview", async () => {
const wrapper = await mountDatasetView("invalid_tab");
const router = wrapper.vm.$router;
expect(router.replace).toHaveBeenCalledWith(`/datasets/${DATASET_ID}`);
});
it("includes preview tab for dataset viewing", async () => {
const wrapper = await mountDatasetView("preview");
@@ -245,13 +238,6 @@ describe("DatasetView", () => {
});
describe("Error state handling", () => {
it("redirects error tab for normal datasets", async () => {
const wrapper = await mountDatasetView("error");
const router = wrapper.vm.$router;
expect(router.replace).toHaveBeenCalledWith(`/datasets/${DATASET_ID}`);
});
it("shows error tab for datasets with error state", async () => {
const wrapper = await mountDatasetView("error", { dataset: errorDataset });
+99 -362
View File
@@ -1,199 +1,89 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BLink, BTab, BTabs } from "bootstrap-vue";
import { computed, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router/composables";
import { BNav, BNavItem } from "bootstrap-vue";
import { computed, ref, watch } from "vue";
import { STATES } from "@/components/History/Content/model/states";
import { usePersistentToggle } from "@/composables/persistentToggle";
import { useDatasetStore } from "@/stores/datasetStore";
import { useDatatypeVisualizationsStore } from "@/stores/datatypeVisualizationsStore";
import { withPrefix } from "@/utils/redirect";
import Heading from "../Common/Heading.vue";
import DatasetError from "../DatasetInformation/DatasetError.vue";
import LoadingSpan from "../LoadingSpan.vue";
import DatasetState from "./DatasetState.vue";
import Heading from "@/components/Common/Heading.vue";
import DatasetAttributes from "@/components/DatasetInformation/DatasetAttributes.vue";
import DatasetDetails from "@/components/DatasetInformation/DatasetDetails.vue";
import VisualizationsList from "@/components/Visualizations/Index.vue";
import VisualizationFrame from "@/components/Visualizations/VisualizationFrame.vue";
library.add(faChevronUp, faChevronDown);
import CenterFrame from "@/entry/analysis/modules/CenterFrame.vue";
const datasetStore = useDatasetStore();
const datatypeVisualizationsStore = useDatatypeVisualizationsStore();
const router = useRouter();
const iframeLoading = ref(true);
// Use persistent toggle for header state
const { toggled: headerCollapsed, toggle: toggleHeaderCollapse } = usePersistentToggle("dataset-header-collapsed");
const headerState = computed(() => (headerCollapsed.value ? "closed" : "open"));
const props = defineProps({
datasetId: {
type: String,
required: true,
},
tab: {
type: String,
default: "preview",
},
interface Props {
datasetId: string;
tab?: "details" | "edit" | "error" | "preview" | "visualize";
}
const props = withDefaults(defineProps<Props>(), {
tab: "preview",
});
const TABS = {
PREVIEW: "preview",
VISUALIZE: "visualize",
DETAILS: "details",
EDIT: "edit",
ERROR: "error",
} as const;
type TabName = (typeof TABS)[keyof typeof TABS];
const TAB_VALUES = Object.values(TABS) as TabName[];
const activeTab = ref<TabName>(TABS.PREVIEW);
const preferredVisualization = ref<string | null>(null);
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));
// Compute display URL based on whether we have a preferred visualization
const displayUrl = computed(() => {
// Standard preview display URL
if (!preferredVisualization.value) {
return withPrefix(`/datasets/${props.datasetId}/display/?preview=true`);
}
// Use preferred visualization
return undefined;
});
const showError = computed(
() => dataset.value && (dataset.value.state === "error" || dataset.value.state === "failed_metadata")
);
const contentState = computed(() => {
return dataset.value && STATES[dataset.value.state] ? STATES[dataset.value.state] : null;
});
const stateText = computed(() => (dataset.value && dataset.value.state) || "");
const hasStateIcon = computed(() => {
return contentState.value && contentState.value.icon;
});
function toggleHeader() {
toggleHeaderCollapse();
}
// Check if the dataset has a preferred visualization by datatype
async function checkPreferredVisualization() {
if (!dataset.value || !dataset.value.file_ext) {
preferredVisualization.value = null;
return;
}
try {
const mapping = await datatypeVisualizationsStore.getPreferredVisualizationForDatatype(dataset.value.file_ext);
if (mapping) {
preferredVisualization.value = mapping.visualization;
} else {
preferredVisualization.value = null;
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;
}
} catch (error) {
preferredVisualization.value = null;
} else {
preferredVisualization.value = undefined;
}
}
function onTabChange(tabName: TabName) {
if (!TAB_VALUES.includes(tabName)) {
console.error("Invalid tab name received:", tabName);
return;
}
if (tabName === TABS.ERROR && !showError.value) {
return;
}
if (tabName === TABS.PREVIEW) {
iframeLoading.value = true;
}
const basePath = `/datasets/${props.datasetId}`;
const targetPath = tabName === TABS.PREVIEW ? basePath : `${basePath}/${tabName}`;
router.push(targetPath);
}
function setActiveTabFromProp() {
const currentTabName = props.tab as string;
if (!TAB_VALUES.includes(currentTabName as TabName)) {
if (currentTabName !== TABS.PREVIEW) {
const previewPath = `/datasets/${props.datasetId}`;
router.replace(previewPath);
}
activeTab.value = TABS.PREVIEW;
return;
}
if (currentTabName === TABS.ERROR && !showError.value) {
if (currentTabName === TABS.ERROR) {
const previewPath = `/datasets/${props.datasetId}`;
router.replace(previewPath);
}
activeTab.value = TABS.PREVIEW;
return;
}
if (activeTab.value !== currentTabName) {
activeTab.value = currentTabName as TabName;
}
}
// Watch the 'tab' prop instead of the route path
watch(() => props.tab, setActiveTabFromProp, { immediate: true });
// Reset iframe loading when datasetId changes
watch(
() => props.datasetId,
() => {
if (activeTab.value === TABS.PREVIEW) {
iframeLoading.value = true;
}
}
);
// Watch for changes to the dataset to check for preferred visualizations
watch(() => dataset.value?.file_ext, checkPreferredVisualization);
onMounted(() => {
// Check for preferred visualization when component is mounted
if (dataset.value) {
checkPreferredVisualization();
}
});
watch(() => dataset.value?.file_ext, checkPreferredVisualization, { immediate: true });
</script>
<template>
<div v-if="dataset && !isLoading" class="dataset-view d-flex flex-column">
<header :key="`dataset-header-${dataset.id}`" class="dataset-header flex-shrink-0">
<Heading h1 separator :collapse="headerState" class="dataset-name" @click="toggleHeader">
<div class="item-identifier">
<span class="hid-box">{{ dataset.hid }}:</span>
<span class="dataset-title">{{ dataset.name }}</span>
<span v-if="contentState" class="state-pill ml-2" :class="`state-${dataset.state}`">
<span v-if="hasStateIcon" class="state-icon mr-1">
<FontAwesomeIcon fixed-width :icon="contentState.icon" :spin="contentState.spin" />
</span>
{{ stateText }}
</span>
</div>
</Heading>
<transition name="header">
<div v-if="headerState === 'open'" class="header-details">
<template>
<LoadingSpan v-if="isLoading || !dataset" message="Loading dataset details" />
<div v-else class="dataset-view d-flex flex-column h-100">
<header :key="`dataset-header-${dataset.id}`" class="dataset-header flex-shrink-0">
<div class="d-flex">
<Heading
h1
separator
inline
size="lg"
class="flex-grow-1"
:collapse="headerState"
@click="toggleHeaderCollapse">
{{ dataset?.hid }}: <span class="font-weight-bold">{{ dataset?.name }}</span>
<span class="dataset-state-header">
<DatasetState :dataset-id="datasetId" />
</span>
</Heading>
</div>
<transition v-if="dataset" name="header">
<div v-show="headerState === 'open'" class="header-details">
<div v-if="dataset.misc_blurb" class="blurb">
<span class="value">{{ dataset.misc_blurb }}</span>
</div>
@@ -206,9 +96,9 @@ onMounted(() => {
<BLink
class="value font-weight-bold"
data-label="Database/Build"
@click="onTabChange(TABS.EDIT)"
>{{ dataset.genome_build }}</BLink
>
:to="`/datasets/${datasetId}/edit`">
{{ dataset.genome_build }}
</BLink>
</span>
<div v-if="dataset.misc_info" class="info">
<span class="value">{{ dataset.misc_info }}</span>
@@ -216,153 +106,56 @@ onMounted(() => {
</div>
</transition>
</header>
<!-- Tab container - make it grow to fill remaining space and handle overflow -->
<div class="dataset-tabs-container flex-grow-1 overflow-hidden">
<BTabs
:key="`tabs-${datasetId}`"
pills
card
lazy
class="h-100 d-flex flex-column"
:value="TAB_VALUES.indexOf(activeTab)"
@input="
(tabIndex) => {
const newTab = TAB_VALUES[tabIndex];
if (newTab) onTabChange(newTab);
}
">
<BTab title="Preview" class="iframe-card" data-test-id="dataset-preview-tab">
<div class="preview-container position-relative h-100">
<!-- Loading indicator for iframe -->
<div v-if="iframeLoading" class="iframe-loading">
<LoadingSpan message="Loading preview" />
</div>
<!-- Show preferred visualization if available -->
<template v-if="preferredVisualization">
<VisualizationFrame
:dataset-id="datasetId"
:visualization="preferredVisualization"
@loaded="iframeLoading = false" />
</template>
<!-- Default iframe preview otherwise -->
<iframe
v-else
:src="displayUrl"
title="galaxy dataset display frame"
class="dataset-preview-iframe"
frameborder="0"
@load="iframeLoading = false"></iframe>
</div>
</BTab>
<BTab title="Visualize" :title-link-attributes="{ title: 'Visualize' }">
<VisualizationsList :dataset-id="datasetId" />
</BTab>
<BTab title="Details" :title-link-attributes="{ title: 'Details' }">
<DatasetDetails :dataset-id="datasetId" />
</BTab>
<BTab title="Edit" :title-link-attributes="{ title: 'Edit' }">
<DatasetAttributes :dataset-id="datasetId" />
</BTab>
<BTab v-if="showError" title="Error Report" :title-link-attributes="{ title: 'Error' }">
<DatasetError :dataset-id="datasetId" />
</BTab>
</BTabs>
<BNav pills class="my-2 p-2 bg-light border-bottom">
<BNavItem title="Preview" :active="tab === 'preview'" :to="`/datasets/${datasetId}/preview`">
Preview
</BNavItem>
<BNavItem
v-if="!showError"
title="Visualize"
:active="tab === 'visualize'"
:to="`/datasets/${datasetId}/visualize`">
Visualize
</BNavItem>
<BNavItem title="Details" :active="tab === 'details'" :to="`/datasets/${datasetId}/details`">
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>
</BNav>
<div v-if="tab === 'preview'" class="h-100">
<VisualizationFrame
v-if="preferredVisualization"
:dataset-id="datasetId"
:visualization="preferredVisualization"
@load="iframeLoading = false" />
<CenterFrame
v-else
:src="`/datasets/${datasetId}/display/?preview=True`"
:is_preview="true"
@load="iframeLoading = false" />
</div>
<div v-else-if="tab === 'visualize'" class="d-flex flex-column overflow-hidden overflow-y">
<VisualizationsList :dataset-id="datasetId" />
</div>
<div v-else-if="tab === 'edit'" class="d-flex flex-column overflow-hidden overflow-y mt-2">
<DatasetAttributes :dataset-id="datasetId" />
</div>
<div v-else-if="tab === 'details'" class="d-flex flex-column overflow-hidden overflow-y mt-2">
<DatasetDetails :dataset-id="datasetId" />
</div>
<div v-else-if="tab === 'error'" class="d-flex flex-column overflow-hidden overflow-y mt-2">
<DatasetError :dataset-id="datasetId" />
</div>
</div>
<!-- Loading state indicator -->
<div v-else class="loading-message">
<LoadingSpan message="Loading dataset details" />
</div>
</template>
<style lang="scss" scoped>
@import "~bootstrap/scss/_functions.scss";
@import "theme/blue.scss";
.dataset-view {
height: 100%;
}
.dataset-header {
margin-bottom: 1rem;
}
.item-identifier {
display: inline-flex;
align-items: center;
max-width: 100%;
flex-wrap: wrap;
}
.dataset-name {
margin-bottom: 0;
}
.hid-box {
margin-right: 0.5rem;
white-space: nowrap;
}
.dataset-title {
font-weight: bold;
white-space: normal;
word-break: break-word;
}
.state-icon {
display: inline-flex;
align-items: center;
}
.state-pill {
display: inline-flex;
align-items: center;
padding: 0.15rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.85rem;
white-space: nowrap;
}
.state-running,
.state-upload,
.state-setting_metadata,
.state-new,
.state-new_populated_state,
.state-inaccessible {
background-color: $state-running-bg;
color: $state-warning-text;
}
.state-paused,
.state-deferred {
background-color: $state-info-bg;
color: $state-info-text;
}
.state-queued,
.state-placeholder {
background-color: $state-default-bg;
color: $text-color;
}
.state-ok,
.state-empty {
background-color: $state-success-bg;
color: $state-success-text;
}
.state-error,
.state-failed_metadata,
.state-discarded,
.state-failed_populated_state {
background-color: $state-danger-bg;
color: $state-danger-text;
}
.header-details {
margin-top: 0.5rem;
padding-left: 1rem;
max-height: 500px;
opacity: 1;
@@ -379,64 +172,8 @@ onMounted(() => {
opacity: 0;
}
.dataset-tabs-container {
min-height: 0;
}
.dataset-tabs-container :deep(.nav-tabs) {
flex-shrink: 0;
}
.dataset-tabs-container :deep(.tab-content) {
flex-grow: 1;
min-height: 0;
overflow: auto;
}
.dataset-tabs-container :deep(.tab-pane) {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
overflow: auto;
}
.dataset-tabs-container :deep(.iframe-card) {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 0;
overflow: hidden;
}
.preview-container {
position: relative;
flex-grow: 1;
}
.dataset-preview-iframe {
border: none;
width: 100%;
height: 100%;
}
.iframe-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: $brand-light;
z-index: 1;
}
.loading-message {
padding: 2rem;
text-align: center;
font-style: italic;
.dataset-state-header {
font-size: $h5-font-size;
vertical-align: middle;
}
</style>
@@ -92,7 +92,7 @@ onMounted(async () => {
<template>
<div aria-labelledby="dataset-attributes-heading">
<Heading id="dataset-attributes-heading" h1 separator inline size="lg">
<Heading id="dataset-attributes-heading" h1 separator inline size="md">
{{ localize("Edit Dataset Attributes") }}
</Heading>
@@ -5,6 +5,7 @@ import { type HDADetailed } from "@/api";
import { withPrefix } from "@/utils/redirect";
import { bytesToString } from "@/utils/utils";
import Heading from "../Common/Heading.vue";
import HelpText from "../Help/HelpText.vue";
import DatasetHashes from "@/components/DatasetInformation/DatasetHashes.vue";
import DatasetSources from "@/components/DatasetInformation/DatasetSources.vue";
@@ -20,8 +21,9 @@ defineProps<Props>();
<template>
<div v-if="dataset">
<h2 class="h-md">Dataset Information</h2>
<Heading id="dataset-information-heading" v-localize h1 separator inline size="md">
Dataset Information
</Heading>
<table id="dataset-details" class="tabletip info_data_table">
<tbody>
<tr>
@@ -91,7 +91,7 @@ export const STATES: StateMap = {
/** the tool producing this dataset has errored */
error: {
status: "danger",
text: "An error occurred with this dataset:",
text: "An error occurred with this dataset.",
icon: "exclamation-triangle",
},
/** metadata discovery/setting failed or errored (but otherwise ok) */
@@ -1,9 +1,11 @@
<script setup lang="ts">
import { computed } from "vue";
import { computed, ref } from "vue";
import { withPrefix } from "@/utils/redirect";
const emit = defineEmits(["loaded"]);
import LoadingSpan from "@/components/LoadingSpan.vue";
const emit = defineEmits(["load"]);
export interface Props {
datasetId?: string;
@@ -13,6 +15,8 @@ export interface Props {
const props = defineProps<Props>();
const isLoading = ref(true);
const srcWithRoot = computed(() => {
let url = "";
if (props.visualization === "trackster") {
@@ -31,16 +35,43 @@ const srcWithRoot = computed(() => {
return withPrefix(url);
});
function handleLoad() {
isLoading.value = false;
emit("load");
}
</script>
<template>
<iframe
id="galaxy_visualization"
:src="srcWithRoot"
class="center-frame"
frameborder="0"
title="galaxy visualization frame"
width="100%"
height="100%"
@load="emit('loaded')" />
<div class="position-relative h-100">
<div v-if="isLoading" class="iframe-loading bg-light">
<LoadingSpan message="Loading preview" />
</div>
<iframe
id="galaxy_visualization"
:src="srcWithRoot"
class="center-frame"
frameborder="0"
title="galaxy visualization frame"
width="100%"
height="100%"
@load="handleLoad" />
</div>
</template>
<style>
.iframe-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
opacity: 0.9;
pointer-events: none;
}
</style>
-15
View File
@@ -5,7 +5,6 @@ import CollectionEditView from "components/Collections/common/CollectionEditView
import DatasetList from "components/Dataset/DatasetList";
import DatasetView from "components/Dataset/DatasetView";
import DatasetDetails from "components/DatasetInformation/DatasetDetails";
import DatasetError from "components/DatasetInformation/DatasetError";
import FormGeneric from "components/Form/FormGeneric";
import GalaxyWizard from "components/GalaxyWizard";
import HelpTerm from "components/Help/HelpTerm";
@@ -50,7 +49,6 @@ import WorkflowCreate from "components/Workflow/WorkflowCreate";
import WorkflowExport from "components/Workflow/WorkflowExport";
import WorkflowImport from "components/Workflow/WorkflowImport";
import Analysis from "entry/analysis/modules/Analysis";
import CenterFrame from "entry/analysis/modules/CenterFrame";
import Home from "entry/analysis/modules/Home";
import Login from "entry/analysis/modules/Login";
import WorkflowEditorModule from "entry/analysis/modules/WorkflowEditor";
@@ -234,14 +232,6 @@ export function getRouter(Galaxy) {
path: "datasets/list",
component: DatasetList,
},
{
path: "datasets/:datasetId/preview",
component: CenterFrame,
props: (route) => ({
src: `/datasets/${route.params.datasetId}/display/?preview=True`,
isPreview: true,
}),
},
{
path: "datasets/:datasetId/report",
component: ToolReport,
@@ -253,11 +243,6 @@ export function getRouter(Galaxy) {
component: DatasetDetails,
props: true,
},
{
path: "datasets/:datasetId/error",
component: DatasetError,
props: true,
},
{
// Consolidated route for dataset view with optional tab
// Handles /datasets/{id}, /datasets/{id}/details, /datasets/{id}/visualize, etc.
+3 -3
View File
@@ -157,9 +157,9 @@ object_store_details:
dataset_view:
selectors:
_: .dataset-view
edit_tab: "a.nav-link[title='Edit']"
details_tab: "a.nav-link[title='Details']"
visualize_tab: "a.nav-link[title='Visualize']"
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"
history_panel:
menu:
+3 -3
View File
@@ -1991,7 +1991,7 @@ 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("a.nav-link[title='Edit']")
edit_tab_button = self.wait_for_selector_clickable(".nav-item[title='Edit'] > a.nav-link")
edit_tab_button.click()
# Wait for the edit attributes panel to be visible
@@ -2006,14 +2006,14 @@ 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("a.nav-link[title='Details']")
details_tab_button = self.wait_for_selector_clickable(".nav-item[title='Details'] > 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("a.nav-link[title='Visualize']")
visualize_tab_button = self.wait_for_selector_clickable(".nav-item[title='Visualize'] > a.nav-link")
visualize_tab_button.click()
def history_panel_item_view_dataset_details(self, hid):
+1
View File
@@ -286,6 +286,7 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs):
webapp.add_client_route("/datasets/{dataset_id}/preview")
webapp.add_client_route("/datasets/{dataset_id}/report")
webapp.add_client_route("/datasets/{dataset_id}/show_params")
webapp.add_client_route("/datasets/{dataset_id}/visualize")
webapp.add_client_route("/datasets/{dataset_id}")
webapp.add_client_route("/collection/{collection_id}/edit")
webapp.add_client_route("/collection/new_list")