Merge pull request #20190 from dannon/preferred-viz

Visualization-First Display functionality
This commit is contained in:
Marius van den Beek
2025-05-15 15:46:53 +02:00
committed by GitHub
29 changed files with 1416 additions and 160 deletions
+47
View File
@@ -0,0 +1,47 @@
import axios from "axios";
import { withPrefix } from "@/utils/redirect";
import { rethrowSimple } from "@/utils/simple-error";
/**
* Interface for datatype to visualization mapping
*/
export interface DatatypeVisualization {
datatype: string; // Datatype extension (e.g., "h5", "tabular")
visualization: string; // Visualization plugin name
}
/**
* Fetches the list of datatype to visualization mappings
*/
export async function fetchDatatypeVisualizations(): Promise<DatatypeVisualization[]> {
try {
const { data } = await axios.get(withPrefix("/api/datatypes/visualizations"));
return data;
} catch (error) {
rethrowSimple(error);
}
}
/**
* Gets the preferred visualization for a specific datatype
*/
export async function getPreferredVisualization(datatype: string): Promise<DatatypeVisualization | null> {
try {
const url = withPrefix(`/api/datatypes/${datatype}/visualizations`);
const { data } = await axios.get(url);
// If the API returns an array with one item, return that item
if (Array.isArray(data) && data.length === 1) {
return data[0];
}
return data;
} catch (error) {
// Return null if no preferred visualization is found
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
rethrowSimple(error);
}
}
+148
View File
@@ -875,6 +875,54 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/datatypes/{datatype}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Get details for a specific datatype
* @description Gets detailed information about a specific datatype.
*
* Includes information about:
* - Basic properties (description, mime type, etc.)
* - Available converters
* - EDAM mappings
* - Preferred visualization
*/
get: operations["show_api_datatypes__datatype__get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/datatypes/{datatype}/visualizations": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Returns the visualization mapping for a specific datatype
* @description Gets the visualization mapping for a specific datatype.
*
* Mappings are defined in the datatypes_conf.xml configuration file.
*/
get: operations["visualization_for_datatype_api_datatypes__datatype__visualizations_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/display_applications": {
parameters: {
query?: never;
@@ -9225,6 +9273,24 @@ export interface components {
*/
prefix_IRI: string;
};
/** DatatypeVisualizationMapping */
DatatypeVisualizationMapping: {
/**
* Datatype
* @description The datatype extension this visualization applies to
*/
datatype: string;
/**
* Visualization
* @description The visualization plugin to use
*/
visualization: string;
};
/**
* DatatypeVisualizationMappingsList
* @default []
*/
DatatypeVisualizationMappingsList: components["schemas"]["DatatypeVisualizationMapping"][];
/** DatatypesCombinedMap */
DatatypesCombinedMap: {
/**
@@ -23704,6 +23770,88 @@ export interface operations {
};
};
};
show_api_datatypes__datatype__get: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Datatype extension to get information for */
datatype: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Detailed information about a datatype */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Request Error */
"4XX": {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageExceptionModel"];
};
};
/** @description Server Error */
"5XX": {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageExceptionModel"];
};
};
};
};
visualization_for_datatype_api_datatypes__datatype__visualizations_get: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Datatype extension to get visualization mapping for */
datatype: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Visualization mapping for the specified datatype */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DatatypeVisualizationMappingsList"];
};
};
/** @description Request Error */
"4XX": {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageExceptionModel"];
};
};
/** @description Server Error */
"5XX": {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageExceptionModel"];
};
};
};
};
display_applications_index_api_display_applications_get: {
parameters: {
query?: never;
@@ -54,7 +54,7 @@ watch(table, (newValue: string[][]) => {
</BCardTitle>
<div>
<BAlert v-if="errorMessage" show variant="danger">{{ errorMessage }}</BAlert>
<JaggedDataAlert :jaggedDataWarning="jaggedDataWarning" />
<JaggedDataAlert :jagged-data-warning="jaggedDataWarning" />
Select a history datasets, the contents will be loaded as tabular data and made available to the rule based
import utility.
</div>
@@ -0,0 +1,345 @@
import { createTestingPinia } from "@pinia/testing";
import { getLocalVue } from "@tests/jest/helpers";
import { mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import { setActivePinia } from "pinia";
import VueRouter from "vue-router";
import DatasetView from "./DatasetView.vue";
// Mock the datatypeVisualizationsStore
jest.mock("@/stores/datatypeVisualizationsStore", () => ({
useDatatypeVisualizationsStore: jest.fn(() => ({
getPreferredVisualizationForDatatype: jest.fn().mockImplementation((datatype) => {
// Only return a preferred visualization for a specific test datatype
if (datatype === "h5") {
return Promise.resolve({
datatype: "h5",
visualization: "h5web",
});
}
return Promise.resolve(null);
}),
})),
}));
const DATASET_ID = "dataset_id";
const localVue = getLocalVue();
localVue.use(VueRouter);
// Mock dataset
const mockDataset = {
id: DATASET_ID,
name: "Test Dataset",
state: "ok",
file_ext: "txt",
genome_build: "hg38",
misc_blurb: "100 lines",
misc_info: "Additional info",
};
const errorDataset = { ...mockDataset, state: "error" };
const failedMetadataDataset = { ...mockDataset, state: "failed_metadata" };
// Additional states to test
const uploadingDataset = { ...mockDataset, state: "upload" };
const runningDataset = { ...mockDataset, state: "running" };
const pausedDataset = { ...mockDataset, state: "paused" };
// Dataset with preferred visualization
const h5Dataset = { ...mockDataset, file_ext: "h5" };
/**
* Mount the DatasetView component with the specified tab and dataset options
*/
async function mountDatasetView(tab = "preview", options = {}) {
const pinia = createTestingPinia({
initialState: {
datasetStore: {
storedDatasets: {
[DATASET_ID]: options.dataset || mockDataset,
},
loadingDatasets: {},
},
},
stubActions: false,
createSpy: jest.fn,
});
setActivePinia(pinia);
const router = new VueRouter();
router.push = jest.fn();
router.replace = jest.fn();
const wrapper = mount(DatasetView, {
propsData: {
datasetId: DATASET_ID,
tab: tab,
},
localVue,
pinia,
router,
attachTo: document.createElement("div"),
stubs: {
// Only shallow stub certain components
"font-awesome-icon": true,
Heading: {
template: "<div><slot></slot></div>",
props: ["h1", "separator"],
},
BLink: {
template: "<a><slot></slot></a>",
props: ["to"],
},
BTabs: {
template: '<div class="tabs-container"><slot></slot></div>',
props: ["pills", "card", "lazy", "value"],
},
BTab: {
template: '<div class="tab-content"><slot></slot></div>',
props: ["title"],
},
DatasetDetails: true,
VisualizationsList: true,
DatasetAttributes: true,
DatasetError: true,
// Use a stub for the VisualizationFrame component
VisualizationFrame: {
template: '<div class="viz-frame"></div>',
props: ["datasetId", "visualization", "visualizationParams"],
},
},
mocks: {
$store: {
state: {
config: {},
},
},
},
});
await flushPromises();
return wrapper;
}
/**
* Mount the DatasetView component in loading state
*/
async function mountLoadingDatasetView() {
const pinia = createTestingPinia({
initialState: {
datasetStore: {
storedDatasets: {},
loadingDatasets: {
[DATASET_ID]: true,
},
},
},
stubActions: false,
createSpy: jest.fn,
});
setActivePinia(pinia);
const router = new VueRouter();
router.push = jest.fn();
router.replace = jest.fn();
const wrapper = mount(DatasetView, {
propsData: {
datasetId: DATASET_ID,
},
localVue,
pinia,
router,
stubs: {
Heading: true,
BLink: true,
BTabs: true,
BTab: true,
},
mocks: {
$store: {
state: {
config: {},
},
},
},
});
await flushPromises();
return wrapper;
}
describe("DatasetView", () => {
describe("Component mounting and basic functionality", () => {
it("mounts with correct props", async () => {
const wrapper = await mountDatasetView();
expect(wrapper.exists()).toBe(true);
expect(wrapper.props().datasetId).toBe(DATASET_ID);
expect(wrapper.props().tab).toBe("preview");
});
it("shows loading message when dataset is loading", async () => {
const wrapper = await mountLoadingDatasetView();
expect(wrapper.find(".loading-message").exists()).toBe(true);
expect(wrapper.find(".loading-message").text()).toBe("Loading dataset details...");
expect(wrapper.find(".dataset-view").exists()).toBe(false);
});
it("renders dataset information", async () => {
const wrapper = await mountDatasetView();
// Check that the component mounted successfully with the expected data
expect(wrapper.vm.$props.datasetId).toBe(DATASET_ID);
expect(wrapper.vm.$props.tab).toBe("preview");
// Make sure we're properly passing the dataset to the component
const datasetStore = wrapper.vm.$pinia.state.value.datasetStore;
expect(datasetStore.storedDatasets[DATASET_ID]).toBeDefined();
expect(datasetStore.storedDatasets[DATASET_ID].name).toBe("Test Dataset");
});
});
describe("Tab navigation functionality", () => {
it("handles different tabs through props", async () => {
let wrapper = await mountDatasetView("details");
expect(wrapper.props().tab).toBe("details");
wrapper = await mountDatasetView("visualize");
expect(wrapper.props().tab).toBe("visualize");
wrapper = await mountDatasetView("edit");
expect(wrapper.props().tab).toBe("edit");
wrapper = await mountDatasetView("error", { dataset: errorDataset });
expect(wrapper.props().tab).toBe("error");
});
it("updates when tab prop changes", async () => {
const wrapper = await mountDatasetView("details");
expect(wrapper.props().tab).toBe("details");
await wrapper.setProps({ tab: "visualize" });
await flushPromises();
expect(wrapper.props().tab).toBe("visualize");
await wrapper.setProps({ tab: "edit" });
await flushPromises();
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");
expect(wrapper.vm.$props.tab).toBe("preview");
const datasetStore = wrapper.vm.$pinia.state.value.datasetStore;
const dataset = datasetStore.storedDatasets[DATASET_ID];
expect(dataset).toBeDefined();
});
});
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 });
const router = wrapper.vm.$router;
expect(router.replace).not.toHaveBeenCalled();
// Check that we're viewing the error tab
expect(wrapper.vm.$props.tab).toBe("error");
});
it("shows error tab for datasets with failed_metadata state", async () => {
const wrapper = await mountDatasetView("error", { dataset: failedMetadataDataset });
const router = wrapper.vm.$router;
expect(router.replace).not.toHaveBeenCalled();
// Check that we're viewing the error tab
expect(wrapper.vm.$props.tab).toBe("error");
});
it("handles datasets with different states appropriately", async () => {
// Test uploading state, make sure it doesn't blow up.
let wrapper = await mountDatasetView("preview", { dataset: uploadingDataset });
expect(wrapper.exists()).toBe(true);
expect(wrapper.vm.$props.datasetId).toBe(DATASET_ID);
expect(wrapper.vm.$props.tab).toBe("preview");
// Test running state, make sure it doesn't blow up.
wrapper = await mountDatasetView("preview", { dataset: runningDataset });
expect(wrapper.exists()).toBe(true);
expect(wrapper.vm.$props.datasetId).toBe(DATASET_ID);
expect(wrapper.vm.$props.tab).toBe("preview");
// Test paused state, make sure it doesn't blow up.
wrapper = await mountDatasetView("preview", { dataset: pausedDataset });
expect(wrapper.exists()).toBe(true);
expect(wrapper.vm.$props.datasetId).toBe(DATASET_ID);
expect(wrapper.vm.$props.tab).toBe("preview");
});
it.skip("uses preferred visualization for supported datatypes", async () => {
const wrapper = await mountDatasetView("preview", { dataset: h5Dataset });
await flushPromises(); // Wait for preferred visualization check
// Check that the preferredVisualization was set
expect(wrapper.vm.preferredVisualization).toBe("h5web");
// Check that we're using the VisualizationFrame for the preferred visualization
expect(wrapper.findComponent({ name: "VisualizationFrame" }).exists()).toBe(true);
expect(wrapper.find("iframe").exists()).toBe(false);
});
it.skip("falls back to default preview for unsupported datatypes", async () => {
const wrapper = await mountDatasetView("preview");
await flushPromises(); // Wait for preferred visualization check
// No preferred visualization should be set
expect(wrapper.vm.preferredVisualization).toBeNull();
// Check that we're using the default iframe
expect(wrapper.findComponent({ name: "VisualizationFrame" }).exists()).toBe(false);
expect(wrapper.find("iframe").exists()).toBe(true);
expect(wrapper.find("iframe").attributes("src")).toBe(`/datasets/${DATASET_ID}/display/?preview=true`);
});
});
describe("URL and routing", () => {
it("tests navigation behavior", async () => {
const wrapper = await mountDatasetView("preview");
await wrapper.setProps({ tab: "details" });
await flushPromises();
expect(wrapper.vm.$props.tab).toBe("details");
});
it("formats tab URLs correctly", async () => {
const previewUrl = `/datasets/${DATASET_ID}`;
const detailsUrl = `/datasets/${DATASET_ID}/details`;
const visualizeUrl = `/datasets/${DATASET_ID}/visualize`;
const editUrl = `/datasets/${DATASET_ID}/edit`;
const errorUrl = `/datasets/${DATASET_ID}/error`;
expect(previewUrl).toBe(`/datasets/${DATASET_ID}`);
expect(detailsUrl).toBe(`/datasets/${DATASET_ID}/details`);
expect(visualizeUrl).toBe(`/datasets/${DATASET_ID}/visualize`);
expect(editUrl).toBe(`/datasets/${DATASET_ID}/edit`);
expect(errorUrl).toBe(`/datasets/${DATASET_ID}/error`);
});
});
});
@@ -0,0 +1,442 @@
<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 { 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);
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;
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>
</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" />
</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>
@@ -176,7 +176,7 @@ const itemUrls = computed<ItemUrls>(() => {
: null,
};
}
let display = `/datasets/${id}/preview`;
let display = `/datasets/${id}`;
if (props.item.extension == "tool_markdown") {
display = `/datasets/${id}/report`;
}
@@ -300,6 +300,10 @@ function onEdit() {
router.push(itemUrls.value.edit!);
}
function onView() {
router.push(itemUrls.value.view!);
}
function onShowCollectionInfo() {
router.push(itemUrls.value.showDetails!);
}
@@ -429,6 +433,7 @@ function unexpandedClick(event: Event) {
"
@delete="onDelete"
@display="onDisplay"
@view="onView"
@showCollectionInfo="onShowCollectionInfo"
@edit="onEdit"
@undelete="onUndelete"
@@ -34,19 +34,12 @@ const entryPointStore = useEntryPointStore();
const errorMessage = ref("");
const deleteCollectionMenu: Ref<BDropdown | null> = ref(null);
const displayButtonTitle = computed(() => (displayDisabled.value ? "This dataset is not yet viewable." : "Display"));
const displayDisabled = computed(() => ["discarded", "new", "upload", "queued"].includes(props.state));
const editButtonTitle = computed(() => (editDisabled.value ? "This dataset is not yet editable." : "Edit attributes"));
const editDisabled = computed(() =>
["discarded", "new", "upload", "queued", "running", "waiting"].includes(props.state)
);
const displayUrl = computed(() => prependPath(props.itemUrls.display));
const editUrl = computed(() => prependPath(props.itemUrls.edit));
const viewUrl = computed(() => prependPath(props.itemUrls.view));
const isCollection = computed(() => !props.isDataset);
@@ -94,7 +87,7 @@ function onDisplay($event: MouseEvent) {
// Wrap display handler to allow ctrl/meta click to open in new tab
// instead of triggering display.
if ($event.ctrlKey || $event.metaKey) {
window.open(displayUrl.value, "_blank");
window.open(viewUrl.value, "_blank");
} else {
emit("display");
}
@@ -119,13 +112,12 @@ function onDisplay($event: MouseEvent) {
<BButton
v-if="isDataset"
v-b-tooltip.hover
:disabled="displayDisabled"
:title="displayButtonTitle"
title="Display"
tabindex="0"
class="display-btn px-1"
size="sm"
variant="link"
:href="displayUrl"
:href="viewUrl"
@click.prevent.stop="onDisplay($event)">
<icon icon="eye" />
</BButton>
@@ -37,30 +37,17 @@ const showDownloads = computed(() => {
return !props.item.purged && ["ok", "failed_metadata", "error"].includes(props.item.state);
});
const showError = computed(() => {
return props.item.state == "error" || props.item.state == "failed_metadata";
});
const showInfo = computed(() => {
return props.item.accessible;
return props.item.state === "error" || props.item.state === "failed_metadata";
});
const showRerun = computed(() => {
return props.item.accessible && props.item.rerunnable && props.item.creating_job && props.item.state != "upload";
});
const showVisualizations = computed(() => {
// TODO: Check hasViz, if visualizations are activated in the config
return !props.item.purged && ["ok", "failed_metadata", "error", "deferred"].includes(props.item.state);
});
const reportErrorUrl = computed(() => {
return prependPath(props.itemUrls.reportError!);
});
const showDetailsUrl = computed(() => {
return prependPath(props.itemUrls.showDetails!);
});
const rerunUrl = computed(() => {
return prependPath(props.itemUrls.rerun!);
});
const visualizeUrl = computed(() => {
return prependPath(props.itemUrls.visualize!);
});
const downloadUrl = computed(() => {
return prependPath(`api/datasets/${props.item.id}/display?to_ext=${props.item.extension}`);
});
@@ -74,25 +61,17 @@ function onDownload(resource: string) {
window.location.href = resource;
}
function onError() {
router.push(props.itemUrls.reportError!);
function onHighlight() {
emit("toggleHighlights");
}
function onInfo() {
router.push(props.itemUrls.showDetails!);
function onError() {
window.location.href = reportErrorUrl.value;
}
function onRerun() {
router.push(`/root?job_id=${props.item.creating_job}`);
}
function onVisualize() {
router.push(props.itemUrls.visualize!);
}
function onHighlight() {
emit("toggleHighlights");
}
</script>
<template>
@@ -125,15 +104,14 @@ function onHighlight() {
</BButton>
<BButton
v-if="showInfo"
v-if="showHighlight"
v-b-tooltip.hover
class="params-btn px-1"
title="Dataset Details"
class="highlight-btn px-1"
title="Show Related Items"
size="sm"
variant="link"
:href="showDetailsUrl"
@click.prevent.stop="onInfo">
<FontAwesomeIcon :icon="faInfoCircle" />
@click.stop="onHighlight">
<FontAwesomeIcon :icon="faSitemap" />
</BButton>
<BButton
@@ -147,29 +125,6 @@ function onHighlight() {
@click.prevent.stop="onRerun">
<FontAwesomeIcon :icon="faRedo" />
</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
class="highlight-btn px-1"
title="Show Related Items"
size="sm"
variant="link"
@click.stop="onHighlight">
<FontAwesomeIcon :icon="faSitemap" />
</BButton>
</div>
</div>
</div>
@@ -5,4 +5,5 @@ export type ItemUrls = {
visualize?: string;
reportError?: string;
showDetails: string | null;
view?: string;
};
@@ -3,6 +3,8 @@ import { computed } from "vue";
import { withPrefix } from "@/utils/redirect";
const emit = defineEmits(["loaded"]);
export interface Props {
datasetId?: string;
visualization: string;
@@ -26,6 +28,7 @@ const srcWithRoot = computed(() => {
url = `/plugins/visualizations/${props.visualization}/saved?id=${props.visualizationId}`;
}
}
return withPrefix(url);
});
</script>
@@ -38,5 +41,6 @@ const srcWithRoot = computed(() => {
frameborder="0"
title="galaxy visualization frame"
width="100%"
height="100%" />
height="100%"
@load="emit('loaded')" />
</template>
@@ -80,7 +80,7 @@ const emit = defineEmits(["onChange"]);
</div>
<FormCard
v-for="(fieldDefinition, index) in value"
v-bind:key="index"
:key="index"
data-description="field definition block"
class="card"
:title="titleForFieldDefinition(index)">
+6
View File
@@ -23,6 +23,12 @@
</strong>
- See all data tables available in this Galaxy.
</li>
<li>
<strong>
<router-link to="/admin/datatype_visualizations">Datatype Visualizations</router-link>
</strong>
- Configure preferred visualizations for datasets based on their datatype.
</li>
<li>
<strong>
<router-link to="/admin/display_applications">Display Applications</router-link>
+11 -12
View File
@@ -3,7 +3,7 @@ import CitationsList from "components/Citation/CitationsList";
import ClientError from "components/ClientError";
import CollectionEditView from "components/Collections/common/CollectionEditView";
import DatasetList from "components/Dataset/DatasetList";
import DatasetAttributes from "components/DatasetInformation/DatasetAttributes";
import DatasetView from "components/Dataset/DatasetView";
import DatasetDetails from "components/DatasetInformation/DatasetDetails";
import DatasetError from "components/DatasetInformation/DatasetError";
import FormGeneric from "components/Form/FormGeneric";
@@ -230,21 +230,10 @@ export function getRouter(Galaxy) {
component: CollectionEditView,
props: true,
},
{
path: "datasets/:datasetId/edit",
component: DatasetAttributes,
props: true,
},
{
path: "datasets/list",
component: DatasetList,
},
{
path: "datasets/:datasetId/details",
name: "DatasetDetails",
component: DatasetDetails,
props: true,
},
{
path: "datasets/:datasetId/preview",
component: CenterFrame,
@@ -269,6 +258,16 @@ export function getRouter(Galaxy) {
component: DatasetError,
props: true,
},
{
// Consolidated route for dataset view with optional tab
// Handles /datasets/{id}, /datasets/{id}/details, /datasets/{id}/visualize, etc.
path: "datasets/:datasetId/:tab?",
component: DatasetView,
props: (route) => ({
datasetId: route.params.datasetId,
tab: route.params.tab,
}),
},
{
path: "datatypes",
component: AvailableDatatypes,
@@ -0,0 +1,77 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import type { DatatypeVisualization } from "@/api/datatypeVisualizations";
import { fetchDatatypeVisualizations, getPreferredVisualization } from "@/api/datatypeVisualizations";
export const useDatatypeVisualizationsStore = defineStore("datatypeVisualizations", () => {
const visualizationMappings = ref<DatatypeVisualization[]>([]);
const loadingStates = ref<Record<string, boolean>>({});
const errorStates = ref<Record<string, string>>({});
// Computed maps for quick lookups
const mappingByDatatype = computed(() => {
const result: Record<string, DatatypeVisualization> = {};
visualizationMappings.value.forEach((mapping) => {
result[mapping.datatype] = mapping;
});
return result;
});
/**
* Load all datatype visualization mappings
*/
async function loadAllMappings() {
loadingStates.value["all"] = true;
errorStates.value["all"] = "";
try {
visualizationMappings.value = await fetchDatatypeVisualizations();
} catch (error) {
errorStates.value["all"] = error instanceof Error ? error.message : String(error);
} finally {
loadingStates.value["all"] = false;
}
}
/**
* Get the preferred visualization for a specific datatype
*/
async function getPreferredVisualizationForDatatype(datatype: string): Promise<DatatypeVisualization | null> {
// First check our store
if (mappingByDatatype.value[datatype]) {
return mappingByDatatype.value[datatype];
}
// If not in store, query the API
loadingStates.value[datatype] = true;
errorStates.value[datatype] = "";
try {
const result = await getPreferredVisualization(datatype);
// If found, add to our store
if (result) {
visualizationMappings.value.push(result);
}
return result;
} catch (error) {
errorStates.value[datatype] = error instanceof Error ? error.message : String(error);
return null;
} finally {
loadingStates.value[datatype] = false;
}
}
// Note: Update and delete operations removed as they're now configured via XML only
return {
visualizationMappings,
loadingStates,
errorStates,
mappingByDatatype,
loadAllMappings,
getPreferredVisualizationForDatatype,
};
});
+10 -9
View File
@@ -154,6 +154,13 @@ object_store_details:
usage_percent: '.quota-percent-text'
usage_progress: '.quota-usage-bar .progress'
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']"
history_panel:
menu:
labels:
@@ -189,10 +196,7 @@ history_panel:
delete_button: '${_} .delete-btn'
display_button: '${_} .display-btn'
download_button: '${_} .download-btn'
edit_button: '${_} .edit-btn'
info_button: '${_} .params-btn'
rerun_button: '${_} .rerun-btn'
visualize_button: '${_} .visualize-btn'
# history content item
content_item:
@@ -214,10 +218,8 @@ history_panel:
# Title buttons...
display_button: '${_} .display-btn'
edit_button: '${_} .edit-btn'
delete_button: '${_} .delete-btn'
rerun_button: '${_} .rerun-btn'
visualize_button: '${_} .visualize-btn'
collection_job_details_button: '${_} .collection-job-details-btn'
# Action buttons...
@@ -347,14 +349,13 @@ storage_dashboard:
edit_dataset_attributes:
selectors:
database_build_dropdown: '[data-label="Database/Build"]'
_: '[aria-labelledby="dataset-attributes-heading"]'
name_input: '[aria-labelledby="dataset-attributes-heading"] input#name'
info_input: '[aria-labelledby="dataset-attributes-heading"] textarea#info'
annotation_input: '[aria-labelledby="dataset-attributes-heading"] textarea#annotation'
datatypes_tab: '.dataset-edit-datatype-tab'
datatype_dropdown: ' #datatype .multiselect'
dbkey_dropdown: '#dbkey .multiselect'
save_button: '#dataset-attributes-default-save'
auto_detect_datatype_button: '#dataset-attributes-autodetect-datatype'
alert: '.dataset-attributes-alert'
@@ -449,7 +450,7 @@ collection_builders:
forward_datasets: ".forward-column .column-datasets"
reverse_datasets: ".reverse-column .column-datasets"
name: "input.collection-name"
modals:
selectors:
create: "button.create-collection"
@@ -1288,4 +1289,4 @@ zip_import_wizard:
wizard_import_button: '.wizard-actions .go-next-btn.btn-primary'
select_file:
type: xpath
selector: "//div[@role='button' and contains(@id, 'g-card-${file_path}')]"
selector: "//div[@role='button' and contains(@id, 'g-card-${file_path}')]"
+7 -2
View File
@@ -72,9 +72,14 @@ steps:
component: history_panel.item(hid=1,state=deferred).datatype
intro: Unless manually set, normally this datatype is based on the contents of the dataset. For deferred datasets, the file name is used to make this inference. Be sure to specify a file type during upload if this is incorrect.
- title: "View dataset"
element: "#current-history-panel div.content-item .display-btn"
intro: "To see details about your dataset click on the eye icon."
postclick: true
- title: Dataset Details
component: history_panel.item(hid=1,state=deferred).info_button
intro: Clicking the info button we can see more information about how our deferred dataset is stored and what it targets.
component: dataset_view.details_tab
intro: Clicking the Details button we can see more information about how our deferred dataset is stored and what it targets.
postclick: true
- title: Dataset Source URI
+15 -14
View File
@@ -91,28 +91,29 @@ steps:
A running tool is shown in yellow and if your calculation is done, it turns green. If your dataset is
grey this means you are queued and need to wait until your tool can be started. If your dataset turns into red, an error has occurred. Please report the bug to the Galaxy team with the bug report button."
- title: "View dataset"
element: "#current-history-panel div.content-item .display-btn"
intro: "View your dataset by clicking the eye button."
- title: "Rename dataset"
element: "#current-history-panel div.content-item .edit-btn"
intro: "Rename your dataset by clicking the pencil button."
- title: "Dataset information"
element: "div.content-item .content-title"
intro: "This is your dataset. You can get more informations and options like different visualizations by clicking on it."
postclick: true
- title: "View dataset"
element: "#current-history-panel div.content-item .display-btn"
intro: "View your dataset by clicking the eye button."
postclick: true
- title: "Dataset information"
component: dataset_view.details_tab
intro: "Clicking on your dataset provides you with more information regarding your dataset (e.g. filetype or size)."
preclick: true
- title: "Rename dataset"
component: dataset_view.edit_tab
intro: "Rename your dataset by clicking the pencil button."
- title: "Remove dataset"
element: "#current-history-panel div.content-item .delete-btn"
intro: "Delete your dataset by clicking the trash-button."
- title: "Dataset information"
element: "#current-history-panel div.content-item .params-btn"
intro: "Clicking on your dataset provides you with more information regarding your dataset (e.g. filetype or size)."
preclick: true
- title: "Re-run tool"
element: "#current-history-panel div.content-item .rerun-btn"
intro: "By clicking the reload button, you can re-run your tool again (e.g. with different parameters or on another dataset)."
@@ -125,7 +126,7 @@ steps:
- title: "Workflow"
component: workflows.activity
intro: "Create, manage, import, export and share your <b>Workflows</b>."
- title: "Visualization"
component: visualization.activity
intro: "Galaxy offers a lot of visualizations to review your data!"
+7 -7
View File
@@ -49,19 +49,19 @@ steps:
- element: "#current-history-panel div.content-item .download-btn"
title: "Download your dataset"
intro: "You can download every dataset by using the floppy disc symbol."
- element: "#current-history-panel div.content-item .params-btn"
title: "Even more information"
intro: "Get an overview of all metadata associated with your dataset by using the Information symbol."
preclick: true
intro: "You can download every dataset by using the floppy disk symbol."
- element: "#current-history-panel div.content-item .display-btn"
title: "Inspect your data"
intro: "The eye symbol can be used to look at your data."
preclick: true
- element: "#current-history-panel div.content-item .edit-btn"
- component: dataset_view.details_tab
title: "Even more information"
intro: "Get an overview of all metadata associated with your dataset by using the Information symbol."
preclick: true
- component: dataset_view.edit_tab
title: "Edit metadata"
intro: "With the pencil button you can edit metadata attributes of your dataset, like the associated filetype or the dataset name."
preclick: true
@@ -35,12 +35,12 @@ steps:
- component: history_panel
intro: "This is your history. It contains our matrix being uploaded."
- component: history_panel.item(hid=1,state=ok).title
intro: "Clicking on a dataset expands its options."
- element: "#current-history-panel div.content-item .display-btn"
intro: "Displaying the dataset will show the dataset preview as well as additional options."
postclick: true
- component: history_panel.item(hid=1,state=ok).visualize_button
intro: "Clicking the diagram-icon will visualize your dataset."
- component: dataset_view.visualize_tab
intro: "Clicking the Visualize tab will list all compatible visualizations for your dataset."
postclick: true
- component: visualization.matched_plugin(id=plotly_surface)
@@ -42,6 +42,7 @@
<converter file="bam_to_bai.xml" target_datatype="bai"/>
<converter file="bam_to_bigwig_converter.xml" target_datatype="bigwig"/>
<converter file="to_qname_sorted_bam.xml" target_datatype="qname_sorted.bam"/>
<visualization plugin="igv" />
<display file="ucsc/bam.xml"/>
<display file="ensembl/ensembl_bam.xml"/>
<display file="igv/bam.xml"/>
@@ -141,8 +142,12 @@
<datatype extension="d3_hierarchy" type="galaxy.datatypes.text:Json" mimetype="application/json" subclass="true" display_in_upload="true"/>
<datatype extension="cytoscapejson" type="galaxy.datatypes.text:CytoscapeJson" mimetype="application/json" display_in_upload="true" description="Cytoscape JSON format for network visualization, typically containing 'nodes' and 'edges' in a JSON object." description_url="https://js.cytoscape.org/#notation/elements-json" />
<datatype extension="imgt.json" type="galaxy.datatypes.text:ImgtJson" mimetype="application/json" display_in_upload="True"/>
<datatype extension="geojson" type="galaxy.datatypes.text:GeoJson" mimetype="application/json" display_in_upload="True"/>
<datatype extension="vitesscejson" type="galaxy.datatypes.text:VitessceJson" mimetype="application/json" display_in_upload="True"/>
<datatype extension="geojson" type="galaxy.datatypes.text:GeoJson" mimetype="application/json" display_in_upload="True">
<visualization plugin="openlayers" />
</datatype>
<datatype extension="vitesscejson" type="galaxy.datatypes.text:VitessceJson" mimetype="application/json" display_in_upload="True">
<visualization plugin="vitessce" />
</datatype>
<datatype extension="data_manager_json" type="galaxy.datatypes.text:DataManagerJson" mimetype="application/json" subclass="true" display_in_upload="false"/>
<datatype extension="dbn" type="galaxy.datatypes.sequence:DotBracket" display_in_upload="true" description="Dot-Bracket format is a text-based format for storing both an RNA sequence and its corresponding 2D structure." description_url="https://wiki.galaxyproject.org/Learn/Datatypes#Dbn"/>
<datatype extension="fai" type="galaxy.datatypes.tabular:Tabular" display_in_upload="true" subclass="true" description="A Fasta Index File is a text file consisting of lines each with five TAB-delimited columns : Name, Length, offset, linebases, Linewidth" description_url="http://www.htslib.org/doc/faidx.html"/>
@@ -221,6 +226,7 @@
<datatype extension="h5" type="galaxy.datatypes.binary:H5" mimetype="application/octet-stream" display_in_upload="true">
<infer_from suffix="h5" />
<infer_from suffix="hdf5" />
<visualization plugin="h5web" />
</datatype>
<datatype extension="scool" type="galaxy.datatypes.binary:H5" mimetype="application/octet-stream" display_in_upload="true" subclass="true"/>
<datatype extension="grib" type="galaxy.datatypes.binary:Grib" mimetype="application/octet-stream" display_in_upload="true"/>
@@ -280,6 +286,7 @@
<datatype extension="tiff" type="galaxy.datatypes.images:Tiff" mimetype="image/tiff" display_in_upload="true">
<infer_from suffix="tiff" />
<infer_from suffix="tif" />
<visualization plugin="tiffviewer" />
<display file="image/avivator.xml"/>
</datatype>
<datatype extension="tf2" type="galaxy.datatypes.images:Tiff" subclass="true" display_in_upload="false"/>
@@ -1148,6 +1155,7 @@
<!-- rdeval types -->
<datatype extension="rd" type="galaxy.datatypes.binary:Binary" mimetype="application/octet-stream" display_in_upload="true" subclass="true" description="Rdeval read sketch"/>
</registration>
<sniffers>
<!--
The order in which Galaxy attempts to determine data types is
+35
View File
@@ -11,6 +11,19 @@ from pydantic import (
RootModel,
)
__all__ = [
"CompositeFileInfo",
"DatatypeDetails",
"DatatypesMap",
"DatatypesCombinedMap",
"DatatypeConverter",
"DatatypeConverterList",
"DatatypeEDAMDetails",
"DatatypesEDAMDetailsDict",
"DatatypeVisualizationMapping",
"DatatypeVisualizationMappingsList",
]
class CompositeFileInfo(BaseModel):
name: str = Field(..., title="Name", description="The name of this composite file") # Mark this field as required
@@ -130,3 +143,25 @@ class DatatypesEDAMDetailsDict(RootModel):
title="Dict of EDAM details for formats",
default={},
)
class DatatypeVisualizationMapping(BaseModel):
datatype: str = Field(
..., # Mark this field as required
title="Datatype",
description="The datatype extension this visualization applies to",
examples=["bam", "h5", "vcf"],
)
visualization: str = Field(
..., # Mark this field as required
title="Visualization",
description="The visualization plugin to use",
examples=["igv", "trackster", "vitessce"],
)
class DatatypeVisualizationMappingsList(RootModel):
root: List[DatatypeVisualizationMapping] = Field(
title="List of datatype visualization mappings",
default=[],
)
+47
View File
@@ -103,6 +103,8 @@ class Registry:
self._edam_formats_mapping = None
self._edam_data_mapping = None
self._converters_by_datatype = {}
# Datatype visualization mappings
self.visualization_mappings: Dict[str, Dict[str, Any]] = {}
# Build sites
self.build_sites = {}
self.display_sites = {}
@@ -314,6 +316,24 @@ class Registry:
self.datatypes_by_extension[extension].add_composite_file(
name, optional=optional, mimetype=mimetype
)
# Check for preferred visualization definitions
for visualization_elem in elem.findall("visualization"):
plugin = visualization_elem.get("plugin", None)
# Skip incomplete visualization definitions
if not plugin:
self.log.warning(
"Incomplete visualization definition (missing plugin) for datatype %s", extension
)
continue
# Store the mapping
self.visualization_mappings[extension] = {
"visualization": plugin,
}
self.log.debug("Loaded preferred visualization definition: %s -> %s", extension, plugin)
for _display_app in elem.findall("display"):
if elem not in self.display_app_containers:
self.display_app_containers.append(elem)
@@ -417,6 +437,7 @@ class Registry:
# Load build sites
if use_build_sites:
self._load_build_sites(root)
self.set_default_values()
def append_to_sniff_order() -> None:
@@ -490,6 +511,32 @@ class Registry:
def get_display_sites(self, site_type):
return self.display_sites.get(site_type, [])
def get_preferred_visualization(self, datatype_extension):
"""
Get the preferred visualization mapping for a specific datatype extension.
Returns a dictionary with 'visualization' and 'default_params' keys, or None if no mapping exists.
Preferred visualizations are defined inline within each datatype definition in the
datatypes_conf.xml configuration file. These mappings determine which visualization plugin
should be used by default when viewing datasets of a specific type.
Example configuration:
<datatype extension="bam" type="galaxy.datatypes.binary:Bam" mimetype="application/octet-stream" display_in_upload="true">
<visualization plugin="igv" />
</datatype>
"""
return self.visualization_mappings.get(datatype_extension)
def get_all_visualization_mappings(self):
"""
Get all datatype to visualization mappings.
Returns a dictionary where keys are datatype extensions and values are mapping configurations.
Mappings are defined inline within each datatype definition in the datatypes_conf.xml
configuration file and are read-only.
"""
return self.visualization_mappings
def load_datatype_sniffers(
self,
root: Element,
+46
View File
@@ -13,6 +13,8 @@ from galaxy.datatypes._schema import (
DatatypesCombinedMap,
DatatypesEDAMDetailsDict,
DatatypesMap,
DatatypeVisualizationMapping,
DatatypeVisualizationMappingsList,
)
from galaxy.datatypes.data import Data
from galaxy.datatypes.registry import Registry
@@ -122,15 +124,59 @@ def view_edam_data(
return datatypes_registry.edam_data
def view_visualization_mappings(
datatypes_registry: Registry, datatype: Optional[str] = None
) -> DatatypeVisualizationMappingsList:
"""
Get datatype visualization mappings from the registry.
Args:
datatypes_registry: The datatypes registry
datatype: If provided, return only the mapping for this datatype extension
Returns:
A list of datatype visualization mappings
"""
mappings = []
# Get all mappings
all_mappings = datatypes_registry.get_all_visualization_mappings()
# Filter for a specific datatype if requested
if datatype and datatype in all_mappings:
mapping_info = all_mappings[datatype]
mappings.append(
{
"datatype": datatype,
"visualization": mapping_info["visualization"],
}
)
elif not datatype:
for dt, mapping_info in all_mappings.items():
mappings.append(
{
"datatype": dt,
"visualization": mapping_info["visualization"],
}
)
return parse_obj_as(DatatypeVisualizationMappingsList, mappings)
__all__ = (
"DatatypeConverterList",
"DatatypeDetails",
"DatatypesCombinedMap",
"DatatypesEDAMDetailsDict",
"DatatypesMap",
"DatatypeVisualizationMapping",
"DatatypeVisualizationMappingsList",
"view_index",
"view_mapping",
"view_types_and_mapping",
"view_sniffers",
"view_converters",
"view_edam_formats",
"view_edam_data",
"view_visualization_mappings",
)
+29 -6
View File
@@ -1987,16 +1987,39 @@ class NavigatesGalaxy(HasDriver):
self.history_panel_wait_for_hid_state(1, "ok", multi_history_panel=True)
def history_panel_item_edit(self, hid):
item = self.history_panel_item_component(hid=hid)
item.edit_button.wait_for_and_click()
self.display_dataset(hid)
# 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.click()
# Wait for the edit attributes panel to be visible
self.components.edit_dataset_attributes._.wait_for_visible()
def history_panel_item_view_dataset_details(self, hid):
def display_dataset(self, hid):
item = self.history_panel_item_component(hid=hid)
item.dataset_operations.wait_for_visible()
item.info_button.wait_for_and_click()
item.display_button.wait_for_and_click()
# Wait for the DatasetView component to load
self.wait_for_selector_visible(".dataset-view")
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.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.click()
def history_panel_item_view_dataset_details(self, hid):
self.display_dataset(hid)
self.show_dataset_details(hid)
def history_panel_item_click_visualization_menu(self, hid):
viz_button_selector = f"{self.history_panel_item_selector(hid)} .visualizations-dropdown"
self.wait_for_and_click_selector(viz_button_selector)
@@ -2054,7 +2077,7 @@ class NavigatesGalaxy(HasDriver):
button_component.wait_for_and_click()
def hda_click_details(self, hid: int):
self.hda_click_primary_action_button(hid, "info")
self.history_panel_item_view_dataset_details(hid)
def history_panel_click_item_title(self, hid, **kwds):
item_component = self.history_panel_item_component(hid=hid)
+94 -1
View File
@@ -11,7 +11,11 @@ from typing import (
Union,
)
from fastapi import Query
from fastapi import (
Path,
Query,
Response,
)
from galaxy.datatypes.registry import Registry
from galaxy.managers.datatypes import (
@@ -20,12 +24,14 @@ from galaxy.managers.datatypes import (
DatatypesCombinedMap,
DatatypesEDAMDetailsDict,
DatatypesMap,
DatatypeVisualizationMappingsList,
view_converters,
view_edam_data,
view_edam_formats,
view_index,
view_mapping,
view_sniffers,
view_visualization_mappings,
)
from . import (
depends,
@@ -165,3 +171,90 @@ class FastAPIDatatypes:
"""Gets a map of datatypes and their corresponding EDAM data.
EDAM data contains the EDAM iri, label, and definition."""
return view_edam_data(self.datatypes_registry, True)
@router.get(
"/api/datatypes/{datatype}/visualizations",
public=True,
summary="Returns the visualization mapping for a specific datatype",
response_description="Visualization mapping for the specified datatype",
response_model=DatatypeVisualizationMappingsList,
)
async def visualization_for_datatype(
self,
datatype: str = Path(
...,
title="Datatype",
description="Datatype extension to get visualization mapping for",
examples=["bam", "h5"],
),
) -> DatatypeVisualizationMappingsList:
"""Gets the visualization mapping for a specific datatype.
Mappings are defined in the datatypes_conf.xml configuration file.
"""
return view_visualization_mappings(self.datatypes_registry, datatype)
@router.get(
"/api/datatypes/{datatype}",
public=True,
summary="Get details for a specific datatype",
response_description="Detailed information about a datatype",
)
async def show(
self,
datatype: str = Path(
...,
title="Datatype",
description="Datatype extension to get information for",
examples=["bam", "h5", "vcf"],
),
):
"""Gets detailed information about a specific datatype.
Includes information about:
- Basic properties (description, mime type, etc.)
- Available converters
- EDAM mappings
- Preferred visualization
"""
# Get the datatype object
dt_object = self.datatypes_registry.get_datatype_by_extension(datatype)
if dt_object is None:
return Response(status_code=404, content=f"Datatype '{datatype}' not found")
# Basic information
result = {
"extension": datatype,
"description": getattr(dt_object, "description", None),
"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),
}
# Add composite files if applicable
composite_files = getattr(dt_object, "composite_files", None)
if composite_files:
result["composite_files"] = [{"name": k, **v.dict()} for k, v in composite_files.items()]
# Add EDAM information if available
edam_format = self.datatypes_registry.edam_formats.get(datatype)
if edam_format:
result["edam_format"] = edam_format
edam_data = self.datatypes_registry.edam_data.get(datatype)
if edam_data:
result["edam_data"] = edam_data
# Add converter information
converters = self.datatypes_registry.get_converters_by_datatype(datatype)
if converters:
result["converters"] = list(converters.keys())
# Add preferred visualization if any
preferred_viz = self.datatypes_registry.get_preferred_visualization(datatype)
if preferred_viz:
result["preferred_visualization"] = {
"visualization": preferred_viz["visualization"],
}
return result
+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}")
webapp.add_client_route("/collection/{collection_id}/edit")
webapp.add_client_route("/collection/new_list")
webapp.add_client_route("/jobs/submission/success")
@@ -46,7 +46,7 @@ class TestCollectionEdit(SeleniumTestCase):
self.history_panel_ensure_showing_item_details(1)
item = self.history_panel_item_component(hid=1)
item.datatype.wait_for_visible()
assert item.datatype.wait_for_text() == dataNew
self._wait_on(lambda _: item.datatype.wait_for_text() == dataNew)
def _create_simple_list_collection(self, filename, ext):
self.perform_upload(self.get_filename(filename), ext=ext)
@@ -49,7 +49,7 @@ class TestHistoryDatasetState(SeleniumTestCase, UsesHistoryItemAssertions):
item.dbkey.wait_for_and_click()
self.sleep_for(self.wait_types.UX_RENDER)
self.screenshot("history_panel_edit_dbkey_post_click")
self.components.edit_dataset_attributes.database_build_dropdown.wait_for_and_click()
self.components.edit_dataset_attributes.dbkey_dropdown.wait_for_and_click()
# choose database option from 'Database/Build' dropdown, that equals to dbkey_text
self.components.edit_dataset_attributes.dbkey_dropdown_results.dbkey_dropdown_option(
dbkey_text=TEST_DBKEY_TEXT
@@ -73,8 +73,6 @@ class TestHistoryDatasetState(SeleniumTestCase, UsesHistoryItemAssertions):
self.history_panel_wait_for_hid_state(FIRST_HID, state="discarded", allowed_force_refreshes=1)
self.history_panel_click_item_title(hid=FIRST_HID, wait=True)
self.screenshot("history_panel_dataset_discarded")
# Next if is a hack for recent changes to beta history...
# https://github.com/galaxyproject/galaxy/pull/13477/files#r823842897
self._assert_downloadable(FIRST_HID, is_downloadable=False)
self.history_panel_item_view_dataset_details(FIRST_HID)
@@ -127,7 +125,7 @@ class TestHistoryDatasetState(SeleniumTestCase, UsesHistoryItemAssertions):
def _assert_downloadable(self, hid, is_downloadable=True):
item = self.history_panel_item_component(hid=hid)
item.dataset_operations.wait_for_visible()
item.info_button.wait_for_visible()
item.highlight_button.wait_for_visible()
if is_downloadable:
assert item.download_button.is_displayed
else:
@@ -3,25 +3,21 @@ from galaxy_test.base.populators import (
skip_without_visualization_plugin,
)
from .framework import (
managed_history,
selenium_test,
SeleniumTestCase,
)
class TestVisualizations(SeleniumTestCase):
ensure_registered = True
@skip_without_datatype("png")
@skip_without_visualization_plugin("annotate_image")
@selenium_test
@managed_history
def test_charts_image_annotate(self):
hid = 1
self.perform_upload(self.get_filename("454Score.png"))
self.wait_for_history()
dataset_component = self.history_panel_click_item_title(hid, wait=True)
dataset_component.visualize_button.wait_for_and_click()
self.history_panel_wait_for_hid_state(hid, state="ok")
self.show_dataset_visualizations(hid)
self.components.visualization.matched_plugin(id="annotate_image").wait_for_visible()
self.screenshot("visualization_plugins_png")
@@ -31,32 +27,13 @@ class TestVisualizations(SeleniumTestCase):
self.wait_for_selector("#image-annotate")
self.screenshot("visualization_plugin_charts_image_annotate")
@managed_history
@selenium_test
@skip_without_visualization_plugin("nvd3_bar")
def test_charts_nvd3_bar(self):
hid = 1
self.perform_upload(self.get_filename("1.sam"))
dataset_component = self.history_panel_click_item_title(hid, wait=True)
dataset_component.visualize_button.wait_for_and_click()
self.components.visualization.matched_plugin(id="nvd3_bar").wait_for_visible()
self.screenshot("visualization_plugins_sam")
self.components.visualization.matched_plugin(id="nvd3_bar").wait_for_and_click()
with self.visualization_panel():
self.wait_for_selector("g.nvd3")
self.screenshot("visualization_plugin_charts_nvd3_bar_landing")
@managed_history
@selenium_test
@skip_without_visualization_plugin("h5web")
def test_charts_h5web(self):
hid = 1
self.perform_upload(self.get_filename("chopper.h5"))
self.history_panel_wait_for_hid_ok(hid)
dataset_component = self.history_panel_click_item_title(hid, wait=True)
dataset_component.visualize_button.wait_for_and_click()
self.history_panel_wait_for_hid_state(hid, state="ok")
self.show_dataset_visualizations(hid)
self.components.visualization.matched_plugin(id="h5web").wait_for_visible()
self.screenshot("visualization_plugins_h5")