Unverified Commit 12396d09 authored by Dannon's avatar Dannon Committed by GitHub
Browse files

Merge pull request #20290 from guerler/adjust_dataset_view

[25.0] Touch up Dataset View
parents de116a52 894f97d1
Loading
Loading
Loading
Loading
+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>
+0 −14
Original line number Diff line number Diff line
@@ -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 });

+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>
@@ -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">
        <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;
@@ -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>
+1 −1
Original line number Diff line number Diff line
@@ -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>

+4 −2
Original line number Diff line number Diff line
@@ -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>
Loading