mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-24 12:13:33 +08:00
Merge pull request #20190 from dannon/preferred-viz
Visualization-First Display functionality
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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}')]"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=[],
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user