Loading client/src/components/Dataset/DatasetState.vue 0 → 100644 +36 −0 Original line number Diff line number Diff line <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> client/src/components/Dataset/DatasetView.test.js +0 −14 Original line number Diff line number Diff line Loading @@ -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"); Loading @@ -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 }); Loading client/src/components/Dataset/DatasetView.vue +97 −360 Original line number Diff line number Diff line <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", }, }); const TABS = { PREVIEW: "preview", VISUALIZE: "visualize", DETAILS: "details", EDIT: "edit", ERROR: "error", } as const; interface Props { datasetId: string; tab?: "details" | "edit" | "error" | "preview" | "visualize"; } type TabName = (typeof TABS)[keyof typeof TABS]; const TAB_VALUES = Object.values(TABS) as TabName[]; const props = withDefaults(defineProps<Props>(), { tab: "preview", }); 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; } if (dataset.value && dataset.value.file_ext) { try { const mapping = await datatypeVisualizationsStore.getPreferredVisualizationForDatatype(dataset.value.file_ext); const mapping = await datatypeVisualizationsStore.getPreferredVisualizationForDatatype( dataset.value.file_ext ); if (mapping) { preferredVisualization.value = mapping.visualization; } else { preferredVisualization.value = null; preferredVisualization.value = undefined; } } catch (error) { preferredVisualization.value = null; } } function onTabChange(tabName: TabName) { if (!TAB_VALUES.includes(tabName)) { console.error("Invalid tab name received:", tabName); return; preferredVisualization.value = undefined; } 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; } else { preferredVisualization.value = undefined; } } ); // 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"> <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"> <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" /> <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> {{ stateText }} </span> </div> </Heading> <transition name="header"> <div v-if="headerState === 'open'" class="header-details"> </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> Loading @@ -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> Loading @@ -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"> <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" @loaded="iframeLoading = false" /> </template> <!-- Default iframe preview otherwise --> <iframe @load="iframeLoading = false" /> <CenterFrame v-else :src="displayUrl" title="galaxy dataset display frame" class="dataset-preview-iframe" frameborder="0" @load="iframeLoading = false"></iframe> :src="`/datasets/${datasetId}/display/?preview=True`" :is_preview="true" @load="iframeLoading = false" /> </div> </BTab> <BTab title="Visualize" :title-link-attributes="{ title: 'Visualize' }"> <div v-else-if="tab === 'visualize'" class="d-flex flex-column overflow-hidden overflow-y"> <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' }"> </div> <div v-else-if="tab === 'edit'" class="d-flex flex-column overflow-hidden overflow-y mt-2"> <DatasetAttributes :dataset-id="datasetId" /> </BTab> <BTab v-if="showError" title="Error Report" :title-link-attributes="{ title: 'Error' }"> <DatasetError :dataset-id="datasetId" /> </BTab> </BTabs> </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> <!-- 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; Loading @@ -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> client/src/components/DatasetInformation/DatasetAttributes.vue +1 −1 Original line number Diff line number Diff line Loading @@ -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> Loading client/src/components/DatasetInformation/DatasetInformation.vue +4 −2 Original line number Diff line number Diff line Loading @@ -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"; Loading @@ -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> Loading Loading
client/src/components/Dataset/DatasetState.vue 0 → 100644 +36 −0 Original line number Diff line number Diff line <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>
client/src/components/Dataset/DatasetView.test.js +0 −14 Original line number Diff line number Diff line Loading @@ -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"); Loading @@ -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 }); Loading
client/src/components/Dataset/DatasetView.vue +97 −360 Original line number Diff line number Diff line <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", }, }); const TABS = { PREVIEW: "preview", VISUALIZE: "visualize", DETAILS: "details", EDIT: "edit", ERROR: "error", } as const; interface Props { datasetId: string; tab?: "details" | "edit" | "error" | "preview" | "visualize"; } type TabName = (typeof TABS)[keyof typeof TABS]; const TAB_VALUES = Object.values(TABS) as TabName[]; const props = withDefaults(defineProps<Props>(), { tab: "preview", }); 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; } if (dataset.value && dataset.value.file_ext) { try { const mapping = await datatypeVisualizationsStore.getPreferredVisualizationForDatatype(dataset.value.file_ext); const mapping = await datatypeVisualizationsStore.getPreferredVisualizationForDatatype( dataset.value.file_ext ); if (mapping) { preferredVisualization.value = mapping.visualization; } else { preferredVisualization.value = null; preferredVisualization.value = undefined; } } catch (error) { preferredVisualization.value = null; } } function onTabChange(tabName: TabName) { if (!TAB_VALUES.includes(tabName)) { console.error("Invalid tab name received:", tabName); return; preferredVisualization.value = undefined; } 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; } else { preferredVisualization.value = undefined; } } ); // 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"> <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"> <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" /> <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> {{ stateText }} </span> </div> </Heading> <transition name="header"> <div v-if="headerState === 'open'" class="header-details"> </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> Loading @@ -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> Loading @@ -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"> <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" @loaded="iframeLoading = false" /> </template> <!-- Default iframe preview otherwise --> <iframe @load="iframeLoading = false" /> <CenterFrame v-else :src="displayUrl" title="galaxy dataset display frame" class="dataset-preview-iframe" frameborder="0" @load="iframeLoading = false"></iframe> :src="`/datasets/${datasetId}/display/?preview=True`" :is_preview="true" @load="iframeLoading = false" /> </div> </BTab> <BTab title="Visualize" :title-link-attributes="{ title: 'Visualize' }"> <div v-else-if="tab === 'visualize'" class="d-flex flex-column overflow-hidden overflow-y"> <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' }"> </div> <div v-else-if="tab === 'edit'" class="d-flex flex-column overflow-hidden overflow-y mt-2"> <DatasetAttributes :dataset-id="datasetId" /> </BTab> <BTab v-if="showError" title="Error Report" :title-link-attributes="{ title: 'Error' }"> <DatasetError :dataset-id="datasetId" /> </BTab> </BTabs> </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> <!-- 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; Loading @@ -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>
client/src/components/DatasetInformation/DatasetAttributes.vue +1 −1 Original line number Diff line number Diff line Loading @@ -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> Loading
client/src/components/DatasetInformation/DatasetInformation.vue +4 −2 Original line number Diff line number Diff line Loading @@ -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"; Loading @@ -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> Loading