mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-24 12:13:33 +08:00
Preferred viz rough sketch
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
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
|
||||
defaultParams?: Record<string, string>; // Optional default parameters for the visualization
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a datatype visualization mapping
|
||||
*/
|
||||
export async function updateDatatypeVisualization(mapping: DatatypeVisualization): Promise<DatatypeVisualization> {
|
||||
try {
|
||||
const { data } = await axios.put(withPrefix(`/api/datatypes/visualizations/${mapping.datatype}`), mapping);
|
||||
return data;
|
||||
} catch (error) {
|
||||
rethrowSimple(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a datatype visualization mapping
|
||||
*/
|
||||
export async function deleteDatatypeVisualization(datatype: string): Promise<void> {
|
||||
try {
|
||||
await axios.delete(withPrefix(`/api/datatypes/visualizations/${datatype}`));
|
||||
} 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/visualizations/${datatype}`);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,23 @@ 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",
|
||||
defaultParams: { param1: "value1" },
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}),
|
||||
})),
|
||||
}));
|
||||
|
||||
const DATASET_ID = "dataset_id";
|
||||
const localVue = getLocalVue();
|
||||
localVue.use(VueRouter);
|
||||
@@ -28,6 +45,8 @@ const failedMetadataDataset = { ...mockDataset, state: "failed_metadata" };
|
||||
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
|
||||
@@ -83,6 +102,11 @@ async function mountDatasetView(tab = "preview", options = {}) {
|
||||
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: {
|
||||
@@ -268,6 +292,32 @@ describe("DatasetView", () => {
|
||||
expect(wrapper.vm.$props.datasetId).toBe(DATASET_ID);
|
||||
expect(wrapper.vm.$props.tab).toBe("preview");
|
||||
});
|
||||
|
||||
it("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");
|
||||
expect(wrapper.vm.preferredVisualizationParams).toEqual({ param1: "value1" });
|
||||
|
||||
// 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("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", () => {
|
||||
|
||||
@@ -3,12 +3,13 @@ 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, ref, watch } from "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 Heading from "../Common/Heading.vue";
|
||||
import DatasetError from "../DatasetInformation/DatasetError.vue";
|
||||
@@ -16,10 +17,12 @@ 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);
|
||||
|
||||
@@ -50,10 +53,23 @@ 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 preferredVisualizationParams = ref<Record<string, string> | null>(null);
|
||||
|
||||
const dataset = computed(() => datasetStore.getDataset(props.datasetId));
|
||||
const isLoading = computed(() => datasetStore.isLoadingDataset(props.datasetId));
|
||||
const displayUrl = computed(() => `/datasets/${props.datasetId}/display/?preview=true`);
|
||||
|
||||
// Compute display URL based on whether we have a preferred visualization
|
||||
const displayUrl = computed(() => {
|
||||
// Standard preview display URL
|
||||
if (!preferredVisualization.value) {
|
||||
return `/datasets/${props.datasetId}/display/?preview=true`;
|
||||
}
|
||||
|
||||
// Use preferred visualization
|
||||
return null;
|
||||
});
|
||||
|
||||
const showError = computed(
|
||||
() => dataset.value && (dataset.value.state === "error" || dataset.value.state === "failed_metadata")
|
||||
);
|
||||
@@ -72,6 +88,28 @@ 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;
|
||||
preferredVisualizationParams.value = mapping.defaultParams || null;
|
||||
} else {
|
||||
preferredVisualization.value = null;
|
||||
preferredVisualizationParams.value = null;
|
||||
}
|
||||
} catch (error) {
|
||||
preferredVisualization.value = null;
|
||||
preferredVisualizationParams.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onTabChange(tabName: TabName) {
|
||||
if (!TAB_VALUES.includes(tabName)) {
|
||||
console.error("Invalid tab name received:", tabName);
|
||||
@@ -130,6 +168,16 @@ watch(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 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">
|
||||
@@ -189,9 +237,17 @@ watch(
|
||||
<LoadingSpan message="Loading preview" />
|
||||
</div>
|
||||
|
||||
<!-- Iframe for dataset preview -->
|
||||
<!-- Show preferred visualization if available -->
|
||||
<template v-if="preferredVisualization">
|
||||
<VisualizationFrame
|
||||
:dataset-id="datasetId"
|
||||
:visualization="preferredVisualization"
|
||||
:visualization-params="preferredVisualizationParams" />
|
||||
</template>
|
||||
<!-- Default iframe preview otherwise -->
|
||||
<iframe
|
||||
:src="displayUrl"
|
||||
v-else
|
||||
:src="displayUrl"
|
||||
title="galaxy dataset display frame"
|
||||
class="dataset-preview-iframe"
|
||||
frameborder="0"
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="datatype-visualizations-manager">
|
||||
<h2>Datatype Visualization Mappings</h2>
|
||||
<p class="mb-4">
|
||||
Configure preferred visualizations for datatypes. These visualizations will be used as the default preview
|
||||
when viewing datasets of the specified type.
|
||||
</p>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="isLoading" class="text-center my-5">
|
||||
<font-awesome-icon icon="spinner" pulse size="2x" />
|
||||
<p class="mt-3">Loading datatype visualization mappings...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<BAlert v-if="error" show variant="danger" class="mb-4"> <strong>Error:</strong> {{ error }} </BAlert>
|
||||
|
||||
<!-- Main content -->
|
||||
<div v-if="!isLoading && !error">
|
||||
<!-- Create new mapping form -->
|
||||
<BCard class="mb-4" header="Add New Mapping">
|
||||
<BForm @submit.prevent="addNewMapping">
|
||||
<BFormGroup label="Datatype" label-for="new-datatype">
|
||||
<BFormSelect
|
||||
id="new-datatype"
|
||||
v-model="newMapping.datatype"
|
||||
:options="availableDatatypes"
|
||||
required>
|
||||
<template v-slot:first>
|
||||
<BFormSelectOption :value="null">-- Select Datatype --</BFormSelectOption>
|
||||
</template>
|
||||
</BFormSelect>
|
||||
</BFormGroup>
|
||||
|
||||
<BFormGroup label="Visualization" label-for="new-visualization">
|
||||
<BFormSelect
|
||||
id="new-visualization"
|
||||
v-model="newMapping.visualization"
|
||||
:options="availableVisualizations"
|
||||
required>
|
||||
<template v-slot:first>
|
||||
<BFormSelectOption :value="null">-- Select Visualization --</BFormSelectOption>
|
||||
</template>
|
||||
</BFormSelect>
|
||||
</BFormGroup>
|
||||
|
||||
<!-- Parameters section, can be expanded in the future -->
|
||||
<BButton type="submit" variant="primary" :disabled="!isFormValid">Add Mapping</BButton>
|
||||
</BForm>
|
||||
</BCard>
|
||||
|
||||
<!-- Existing mappings table -->
|
||||
<BCard header="Existing Mappings">
|
||||
<BTable
|
||||
striped
|
||||
hover
|
||||
:items="visualizationMappings"
|
||||
:fields="fields"
|
||||
empty-text="No datatype visualization mappings defined."
|
||||
class="mb-0">
|
||||
<template v-slot:cell(actions)="row">
|
||||
<BButton
|
||||
size="sm"
|
||||
variant="danger"
|
||||
:disabled="isDeleting[row.item.datatype]"
|
||||
@click="confirmDelete(row.item)">
|
||||
<font-awesome-icon v-if="isDeleting[row.item.datatype]" icon="spinner" pulse />
|
||||
<font-awesome-icon v-else icon="trash" />
|
||||
Delete
|
||||
</BButton>
|
||||
</template>
|
||||
</BTable>
|
||||
</BCard>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation modal -->
|
||||
<BModal
|
||||
v-model="showDeleteModal"
|
||||
title="Confirm Deletion"
|
||||
ok-variant="danger"
|
||||
ok-title="Delete"
|
||||
@ok="deleteMapping">
|
||||
<p>
|
||||
Are you sure you want to delete the visualization mapping for datatype
|
||||
<strong>{{ selectedMapping?.datatype }}</strong
|
||||
>?
|
||||
</p>
|
||||
</BModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BAlert,
|
||||
BButton,
|
||||
BCard,
|
||||
BForm,
|
||||
BFormGroup,
|
||||
BFormSelect,
|
||||
BFormSelectOption,
|
||||
BModal,
|
||||
BTable,
|
||||
} from "bootstrap-vue";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
import { type DatatypeVisualization } from "@/api/datatypeVisualizations";
|
||||
import { fetchPlugins } from "@/api/plugins";
|
||||
import { useDatatypeStore } from "@/stores/datatypeStore";
|
||||
import { useDatatypeVisualizationsStore } from "@/stores/datatypeVisualizationsStore";
|
||||
|
||||
const datatypeVisualizationsStore = useDatatypeVisualizationsStore();
|
||||
const datatypeStore = useDatatypeStore();
|
||||
|
||||
// State
|
||||
const isLoading = ref(true);
|
||||
const error = ref("");
|
||||
const availableDatatypes = ref<Array<{ value: string; text: string }>>([]);
|
||||
const availableVisualizations = ref<Array<{ value: string; text: string }>>([]);
|
||||
const newMapping = ref<DatatypeVisualization>({
|
||||
datatype: "",
|
||||
visualization: "",
|
||||
});
|
||||
const showDeleteModal = ref(false);
|
||||
const selectedMapping = ref<DatatypeVisualization | null>(null);
|
||||
const isDeleting = ref<Record<string, boolean>>({});
|
||||
|
||||
// Table configuration
|
||||
const fields = [
|
||||
{ key: "datatype", label: "Datatype", sortable: true },
|
||||
{ key: "visualization", label: "Visualization", sortable: true },
|
||||
{ key: "actions", label: "Actions" },
|
||||
];
|
||||
|
||||
// Computed properties
|
||||
const visualizationMappings = computed(() => {
|
||||
return datatypeVisualizationsStore.visualizationMappings;
|
||||
});
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return newMapping.value.datatype && newMapping.value.visualization;
|
||||
});
|
||||
|
||||
// Initialize component
|
||||
async function initialize() {
|
||||
isLoading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
// Load datatypes
|
||||
await datatypeStore.loadDatatypes();
|
||||
|
||||
// Convert to select options
|
||||
availableDatatypes.value = Object.entries(datatypeStore.datatypes)
|
||||
.map(([ext, info]) => ({
|
||||
value: ext,
|
||||
text: `${ext} - ${info.description || ext}`,
|
||||
}))
|
||||
.sort((a, b) => a.text.localeCompare(b.text));
|
||||
|
||||
// Load visualizations
|
||||
const plugins = await fetchPlugins();
|
||||
availableVisualizations.value = plugins
|
||||
.map((plugin) => ({
|
||||
value: plugin.name,
|
||||
text: plugin.name,
|
||||
}))
|
||||
.sort((a, b) => a.text.localeCompare(b.text));
|
||||
|
||||
// Load existing mappings
|
||||
await datatypeVisualizationsStore.loadAllMappings();
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new mapping
|
||||
async function addNewMapping() {
|
||||
if (!isFormValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await datatypeVisualizationsStore.updateMapping(newMapping.value);
|
||||
|
||||
// Reset form
|
||||
newMapping.value = {
|
||||
datatype: "",
|
||||
visualization: "",
|
||||
};
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm deletion of a mapping
|
||||
function confirmDelete(mapping: DatatypeVisualization) {
|
||||
selectedMapping.value = mapping;
|
||||
showDeleteModal.value = true;
|
||||
}
|
||||
|
||||
// Delete a mapping
|
||||
async function deleteMapping() {
|
||||
if (!selectedMapping.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const datatype = selectedMapping.value.datatype;
|
||||
isDeleting.value[datatype] = true;
|
||||
|
||||
try {
|
||||
await datatypeVisualizationsStore.deleteMapping(datatype);
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
} finally {
|
||||
isDeleting.value[datatype] = false;
|
||||
selectedMapping.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data when component is mounted
|
||||
onMounted(initialize);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.datatype-visualizations-manager {
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -6,6 +6,7 @@ import DataManagerTable from "components/admin/DataManager/DataManagerTable";
|
||||
import DataManagerView from "components/admin/DataManager/DataManagerView";
|
||||
import DataTables from "components/admin/DataTables";
|
||||
import DataTypes from "components/admin/DataTypes";
|
||||
import DatatypeVisualizations from "components/admin/DatatypeVisualizations";
|
||||
import ToolboxDependencies from "components/admin/Dependencies/Landing";
|
||||
import DisplayApplications from "components/admin/DisplayApplications";
|
||||
import ErrorStack from "components/admin/ErrorStack";
|
||||
@@ -47,6 +48,7 @@ export default [
|
||||
},
|
||||
{ path: "data_tables", component: DataTables },
|
||||
{ path: "data_types", component: DataTypes },
|
||||
{ path: "datatype_visualizations", component: DatatypeVisualizations },
|
||||
{ path: "display_applications", component: DisplayApplications },
|
||||
{ path: "error_stack", component: ErrorStack },
|
||||
{
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import type { DatatypeVisualization } from "@/api/datatypeVisualizations";
|
||||
import {
|
||||
deleteDatatypeVisualization,
|
||||
fetchDatatypeVisualizations,
|
||||
getPreferredVisualization,
|
||||
updateDatatypeVisualization,
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a visualization mapping
|
||||
*/
|
||||
async function updateMapping(mapping: DatatypeVisualization) {
|
||||
loadingStates.value[mapping.datatype] = true;
|
||||
errorStates.value[mapping.datatype] = "";
|
||||
|
||||
try {
|
||||
const updatedMapping = await updateDatatypeVisualization(mapping);
|
||||
|
||||
// Update in our store
|
||||
const index = visualizationMappings.value.findIndex((m) => m.datatype === mapping.datatype);
|
||||
if (index >= 0) {
|
||||
visualizationMappings.value[index] = updatedMapping;
|
||||
} else {
|
||||
visualizationMappings.value.push(updatedMapping);
|
||||
}
|
||||
|
||||
return updatedMapping;
|
||||
} catch (error) {
|
||||
errorStates.value[mapping.datatype] = error instanceof Error ? error.message : String(error);
|
||||
throw error;
|
||||
} finally {
|
||||
loadingStates.value[mapping.datatype] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a visualization mapping
|
||||
*/
|
||||
async function deleteMapping(datatype: string) {
|
||||
loadingStates.value[datatype] = true;
|
||||
errorStates.value[datatype] = "";
|
||||
|
||||
try {
|
||||
await deleteDatatypeVisualization(datatype);
|
||||
|
||||
// Remove from our store
|
||||
visualizationMappings.value = visualizationMappings.value.filter((m) => m.datatype !== datatype);
|
||||
} catch (error) {
|
||||
errorStates.value[datatype] = error instanceof Error ? error.message : String(error);
|
||||
throw error;
|
||||
} finally {
|
||||
loadingStates.value[datatype] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
visualizationMappings,
|
||||
loadingStates,
|
||||
errorStates,
|
||||
mappingByDatatype,
|
||||
loadAllMappings,
|
||||
getPreferredVisualizationForDatatype,
|
||||
updateMapping,
|
||||
deleteMapping,
|
||||
};
|
||||
});
|
||||
@@ -1148,6 +1148,23 @@
|
||||
<!-- 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>
|
||||
|
||||
<visualization_mappings>
|
||||
<!--
|
||||
This section maps datatypes to their preferred visualization plugins.
|
||||
These visualizations will be used as the default preview when viewing datasets of the specified type.
|
||||
|
||||
Attributes:
|
||||
- datatype: The file extension/datatype to map
|
||||
- visualization: The visualization plugin to use
|
||||
- default_params: Optional JSON-formatted parameters to pass to the visualization
|
||||
-->
|
||||
<visualization_mapping datatype="h5" visualization="vitessce" />
|
||||
<visualization_mapping datatype="bam" visualization="igv" />
|
||||
<visualization_mapping datatype="vcf" visualization="jbrowse" />
|
||||
<!-- Add more mappings as needed -->
|
||||
</visualization_mappings>
|
||||
|
||||
<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,30 @@ 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"],
|
||||
)
|
||||
default_params: Optional[Dict[str, str]] = Field(
|
||||
default=None,
|
||||
title="Default Parameters",
|
||||
description="Optional parameters to pass to the visualization plugin",
|
||||
)
|
||||
|
||||
|
||||
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 = {}
|
||||
@@ -417,6 +419,9 @@ class Registry:
|
||||
# Load build sites
|
||||
if use_build_sites:
|
||||
self._load_build_sites(root)
|
||||
|
||||
# Load visualization mappings
|
||||
self._load_visualization_mappings(root)
|
||||
self.set_default_values()
|
||||
|
||||
def append_to_sniff_order() -> None:
|
||||
@@ -436,6 +441,60 @@ class Registry:
|
||||
|
||||
append_to_sniff_order()
|
||||
|
||||
def _load_visualization_mappings(self, root):
|
||||
"""
|
||||
Load datatype to visualization mappings from the datatypes_conf.xml file.
|
||||
|
||||
Administrators can define preferred visualizations for datatypes in the
|
||||
datatypes_conf.xml configuration file using the visualization_mappings section.
|
||||
|
||||
For example:
|
||||
|
||||
<visualization_mappings>
|
||||
<visualization_mapping datatype="bam" visualization="igv" />
|
||||
<visualization_mapping datatype="vcf" visualization="jbrowse" />
|
||||
<visualization_mapping datatype="h5" visualization="vitessce" default_params='{"layer":"primary"}' />
|
||||
</visualization_mappings>
|
||||
|
||||
These mappings are read-only and must be manually edited in the configuration file.
|
||||
"""
|
||||
# Clear existing mappings
|
||||
self.visualization_mappings = {}
|
||||
|
||||
# Find the visualization_mappings element
|
||||
mappings_elem = root.find("visualization_mappings")
|
||||
if mappings_elem is None:
|
||||
# No mappings defined, that's okay
|
||||
return
|
||||
|
||||
for mapping_elem in mappings_elem.findall("visualization_mapping"):
|
||||
datatype = mapping_elem.get("datatype", None)
|
||||
visualization = mapping_elem.get("visualization", None)
|
||||
default_params_str = mapping_elem.get("default_params", None)
|
||||
|
||||
# Skip incomplete mappings
|
||||
if not datatype or not visualization:
|
||||
self.log.warning("Incomplete visualization mapping (missing datatype or visualization)")
|
||||
continue
|
||||
|
||||
# Parse default parameters if provided
|
||||
default_params = None
|
||||
if default_params_str:
|
||||
try:
|
||||
default_params = galaxy.util.safe_loads(default_params_str)
|
||||
except Exception:
|
||||
self.log.exception("Error parsing default_params for visualization mapping: %s -> %s",
|
||||
datatype, visualization)
|
||||
continue
|
||||
|
||||
# Store the mapping
|
||||
self.visualization_mappings[datatype] = {
|
||||
"visualization": visualization,
|
||||
"default_params": default_params
|
||||
}
|
||||
|
||||
self.log.debug("Loaded visualization mapping: %s -> %s", datatype, visualization)
|
||||
|
||||
def _load_build_sites(self, root):
|
||||
def load_build_site(build_site_config):
|
||||
# Take in either an XML element or simple dictionary from YAML and add build site for this.
|
||||
@@ -489,6 +548,31 @@ 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 in the datatypes_conf.xml configuration file using
|
||||
the visualization_mappings section. These mappings determine which visualization plugin
|
||||
should be used by default when viewing datasets of a specific type.
|
||||
|
||||
Example configuration:
|
||||
<visualization_mappings>
|
||||
<visualization_mapping datatype="bam" visualization="igv" />
|
||||
</visualization_mappings>
|
||||
"""
|
||||
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 in the datatypes_conf.xml configuration file and are read-only.
|
||||
"""
|
||||
return self.visualization_mappings
|
||||
|
||||
def load_datatype_sniffers(
|
||||
self,
|
||||
|
||||
@@ -11,8 +11,10 @@ from galaxy.datatypes._schema import (
|
||||
DatatypeConverterList,
|
||||
DatatypeDetails,
|
||||
DatatypesCombinedMap,
|
||||
DatatypesEDAMDetailsDict,
|
||||
DatatypesEDAMDetailsDict,
|
||||
DatatypesMap,
|
||||
DatatypeVisualizationMapping,
|
||||
DatatypeVisualizationMappingsList,
|
||||
)
|
||||
from galaxy.datatypes.data import Data
|
||||
from galaxy.datatypes.registry import Registry
|
||||
@@ -122,15 +124,57 @@ 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"],
|
||||
"default_params": mapping_info.get("default_params")
|
||||
})
|
||||
elif not datatype:
|
||||
for dt, mapping_info in all_mappings.items():
|
||||
mappings.append({
|
||||
"datatype": dt,
|
||||
"visualization": mapping_info["visualization"],
|
||||
"default_params": mapping_info.get("default_params")
|
||||
})
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from fastapi import Query
|
||||
from fastapi import Path, Query
|
||||
|
||||
from galaxy.datatypes.registry import Registry
|
||||
from galaxy.managers.datatypes import (
|
||||
@@ -20,12 +20,15 @@ from galaxy.managers.datatypes import (
|
||||
DatatypesCombinedMap,
|
||||
DatatypesEDAMDetailsDict,
|
||||
DatatypesMap,
|
||||
DatatypeVisualizationMapping,
|
||||
DatatypeVisualizationMappingsList,
|
||||
view_converters,
|
||||
view_edam_data,
|
||||
view_edam_formats,
|
||||
view_index,
|
||||
view_mapping,
|
||||
view_sniffers,
|
||||
view_visualization_mappings,
|
||||
)
|
||||
from . import (
|
||||
depends,
|
||||
@@ -165,3 +168,36 @@ 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/visualizations",
|
||||
public=True,
|
||||
summary="Returns a list of all datatype visualization mappings",
|
||||
response_description="List of all datatype visualization mappings",
|
||||
response_model=DatatypeVisualizationMappingsList,
|
||||
)
|
||||
async def visualizations(self) -> DatatypeVisualizationMappingsList:
|
||||
"""Gets a list of all datatype visualization mappings."""
|
||||
return view_visualization_mappings(self.datatypes_registry)
|
||||
|
||||
@router.get(
|
||||
"/api/datatypes/visualizations/{datatype}",
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user