Use bnav instead of btabs

This commit is contained in:
guerler
2025-05-18 10:20:47 -04:00
parent afefbbbc8a
commit f68d183113
+27 -424
View File
@@ -1,442 +1,45 @@
<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 } 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 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 Heading from "@/components/Common/Heading.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"));
interface Props {
username?: string;
datasetId: string;
tab?: "details" | "edit" | "view" | "visualize";
}
const props = defineProps({
datasetId: {
type: String,
required: true,
},
tab: {
type: String,
default: "preview",
},
const props = withDefaults(defineProps<Props>(), {
tab: "view",
username: undefined,
});
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 dataset = computed(() => datasetStore.getDataset(props.datasetId));
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;
}
} catch (error) {
preferredVisualization.value = null;
}
}
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();
}
});
</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>
<LoadingSpan v-if="isLoading"></LoadingSpan>
<div v-else class="d-flex flex-column">
<div class="d-flex">
<Heading h1 separator inline size="lg" class="flex-grow-1 mb-2">
{{ dataset?.hid }}: <span class="font-weight-bold">{{ dataset?.name }}</span>
</Heading>
<transition name="header">
<div v-if="headerState === 'open'" class="header-details">
<div v-if="dataset.misc_blurb" class="blurb">
<span class="value">{{ dataset.misc_blurb }}</span>
</div>
<span v-if="dataset.file_ext" class="datatype">
<span v-localize class="prompt">format</span>
<span class="value font-weight-bold">{{ dataset.file_ext }}</span>
</span>
<span v-if="dataset.genome_build" class="dbkey">
<span v-localize class="prompt">database</span>
<BLink
class="value font-weight-bold"
data-label="Database/Build"
@click="onTabChange(TABS.EDIT)"
>{{ dataset.genome_build }}</BLink
>
</span>
<div v-if="dataset.misc_info" class="info">
<span class="value">{{ dataset.misc_info }}</span>
</div>
</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>
</div>
</div>
<!-- Loading state indicator -->
<div v-else class="loading-message">
<LoadingSpan message="Loading dataset details" />
<BNav pills justified class="mb-2">
<BNavItem :active="tab === 'view'" :to="`/datasets/${datasetId}/view`"> Preview </BNavItem>
<BNavItem :active="tab === 'visualize'" :to="`/datasets/${datasetId}/visualize`"> Visualize </BNavItem>
<BNavItem :active="tab === 'details'" :to="`/datasets/${datasetId}/details`"> Details </BNavItem>
<BNavItem :active="tab === 'edit'" :to="`/datasets/${datasetId}/edit`"> Edit </BNavItem>
</BNav>
<div v-if="tab === 'view'">View</div>
<div v-else-if="tab === 'visualize'">Visualize</div>
<div v-else-if="tab === 'details'">Details</div>
<div v-else>Edit</div>
</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;
transition: all 0.25s ease;
overflow: hidden;
}
.header-enter, /* change to header-enter-from with Vue 3 */
.header-leave-to {
max-height: 0;
margin-top: 0;
padding-top: 0;
padding-bottom: 0;
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;
}
</style>