Merge pull request #20914 from guerler/allow_creating_visualizations

Allow creation of visualizations without dataset
This commit is contained in:
Aysam Guerler
2025-09-17 14:58:23 +03:00
committed by GitHub
9 changed files with 73 additions and 44 deletions
+7 -2
View File
@@ -29,16 +29,21 @@ export interface Plugin {
html: string;
logo?: string;
name: string;
params?: Record<string, ParamType>;
target?: string;
tags?: Array<string>;
tests?: Array<TestType>;
}
export interface ParamType {
required?: boolean;
}
export interface PluginData {
hdas: Array<Dataset>;
}
export interface ParamType {
export interface TestParamType {
ftype?: string;
label?: string;
name: string;
@@ -46,7 +51,7 @@ export interface ParamType {
}
export interface TestType {
param: ParamType;
param: TestParamType;
}
export async function fetchPlugins(datasetId?: string): Promise<Array<Plugin>> {
@@ -4,11 +4,20 @@ import { createPinia, defineStore, setActivePinia } from "pinia";
import { getLocalVue } from "tests/jest/helpers";
import { ref } from "vue";
import { fetchPluginHistoryItems } from "@/api/plugins";
import { fetchPlugin, fetchPluginHistoryItems } from "@/api/plugins";
import VisualizationCreate from "./VisualizationCreate.vue";
import FormCardSticky from "@/components/Form/FormCardSticky.vue";
const PLUGIN = {
name: "scatterplot",
description: "A great scatterplot plugin.",
html: "Scatterplot Plugin",
logo: "/logo.png",
help: "Some help text",
tags: ["tag1", "tag2"],
};
jest.mock("vue-router/composables", () => ({
useRouter: () => ({
push: jest.fn(),
@@ -18,22 +27,13 @@ jest.mock("vue-router/composables", () => ({
jest.mock("@/api/plugins", () => ({
fetchPlugin: jest.fn(() =>
Promise.resolve({
name: "scatterplot",
description: "A great scatterplot plugin.",
html: "Scatterplot Plugin",
logo: "/logo.png",
help: "Some help text",
tags: ["tag1", "tag2"],
params: { dataset_id: { required: true } },
...PLUGIN,
}),
),
fetchPluginHistoryItems: jest.fn(() => Promise.resolve({ hdas: [] })),
}));
jest.mock("./utilities", () => ({
getTestExtensions: jest.fn(() => ["txt"]),
getTestUrls: jest.fn(() => [{ name: "Example", url: "https://example.com/data.txt" }]),
}));
let mockedStore;
jest.mock("@/stores/historyStore", () => ({
useHistoryStore: () => mockedStore,
@@ -95,3 +95,16 @@ it("adds hid to dataset names when fetching history items", async () => {
{ id: "dataset2", name: "102: Second Dataset" },
]);
});
it("displays create new visualization option if dataset is not required", async () => {
fetchPlugin.mockResolvedValueOnce(PLUGIN);
const wrapper = mount(VisualizationCreate, {
localVue,
propsData: {
visualization: "scatterplot",
},
});
await wrapper.vm.$nextTick();
const results = await wrapper.vm.doQuery();
expect(results).toEqual([{ id: "", name: "Open visualization..." }]);
});
@@ -3,12 +3,12 @@ import { storeToRefs } from "pinia";
import { computed, onMounted, type Ref, ref } from "vue";
import { useRouter } from "vue-router/composables";
import { type Dataset, fetchPlugin, fetchPluginHistoryItems, type Plugin } from "@/api/plugins";
import { fetchPlugin, fetchPluginHistoryItems, type Plugin } from "@/api/plugins";
import type { OptionType } from "@/components/SelectionField/types";
import { useMarkdown } from "@/composables/markdown";
import { useHistoryStore } from "@/stores/historyStore";
import { getTestExtensions, getTestUrls } from "./utilities";
import { getRequiresDataset, getTestExtensions, getTestUrls } from "./utilities";
import VisualizationExamples from "./VisualizationExamples.vue";
import Heading from "@/components/Common/Heading.vue";
@@ -27,20 +27,20 @@ const props = defineProps<{
}>();
const errorMessage = ref("");
const formatsVisible = ref(false);
const plugin: Ref<Plugin | undefined> = ref();
const urlData = computed(() => getTestUrls(plugin.value));
const extensions = computed(() => getTestExtensions(plugin.value));
const formatsVisible = ref(false);
function addHidToName(hdas: Array<Dataset>) {
return hdas.map((entry) => ({ id: entry.id, name: `${entry.hid}: ${entry.name}` }));
}
const requiresDataset = computed(() => getRequiresDataset(plugin.value));
const testUrls = computed(() => getTestUrls(plugin.value));
async function doQuery() {
if (currentHistoryId.value && plugin.value) {
const data = await fetchPluginHistoryItems(plugin.value.name, currentHistoryId.value);
return addHidToName(data.hdas);
return [
...(!requiresDataset.value ? [{ id: "", name: `Open visualization...` }] : []),
...data.hdas.map((hda) => ({ id: hda.id, name: `${hda.hid}: ${hda.name}` })),
];
} else {
return [];
}
@@ -51,7 +51,8 @@ async function getPlugin() {
}
function onSelect(dataset: OptionType) {
router.push(`/visualizations/display?visualization=${plugin.value?.name}&dataset_id=${dataset.id}`, {
const query = dataset.id ? `&dataset_id=${dataset.id}` : "";
router.push(`/visualizations/display?visualization=${plugin.value?.name}${query}`, {
// @ts-ignore
title: dataset.name,
});
@@ -73,11 +74,11 @@ defineExpose({ doQuery });
:logo="plugin?.logo"
:name="plugin?.html">
<template v-slot:buttons>
<VisualizationExamples :url-data="urlData" />
<VisualizationExamples :url-data="testUrls" />
</template>
<div class="my-3">
<SelectionField
object-name="Select a dataset..."
object-name="Make a selection..."
object-title="Select to Visualize"
object-type="history_dataset_id"
:object-query="doQuery"
@@ -23,16 +23,17 @@ const iframeRef = ref<HTMLIFrameElement | null>(null);
const srcWithRoot = computed(() => {
let url = "";
if (props.visualization === "trackster") {
if (props.datasetId) {
url = `/visualization/trackster?dataset_id=${props.datasetId}`;
} else {
if (props.visualizationId) {
url = `/visualization/trackster?id=${props.visualizationId}`;
} else {
url = `/visualization/trackster?dataset_id=${props.datasetId}`;
}
} else {
if (props.datasetId) {
url = `/plugins/visualizations/${props.visualization}/show?dataset_id=${props.datasetId}`;
} else {
if (props.visualizationId) {
url = `/plugins/visualizations/${props.visualization}/saved?id=${props.visualizationId}`;
} else {
const query = props.datasetId ? `?dataset_id=${props.datasetId}` : "";
url = `/plugins/visualizations/${props.visualization}/show${query}`;
}
}
@@ -1,7 +1,15 @@
import { getFilename, getTestUrls } from "./utilities";
import { getFilename, getRequiresDataset, getTestUrls } from "./utilities";
describe("Utility Functions", () => {
describe("getTestUrls", () => {
describe("getRequiresDataset and getTestUrls", () => {
it("return wether the visualization requires a dataset or not", () => {
expect(getRequiresDataset({})).toEqual(false);
const plugin = {
params: { dataset_id: { required: true } },
};
expect(getRequiresDataset(plugin)).toEqual(true);
});
it("returns empty array when plugin is undefined", () => {
expect(getTestUrls()).toEqual([]);
});
@@ -1,5 +1,9 @@
import type { Plugin } from "@/api/plugins";
export function getRequiresDataset(plugin?: Plugin): boolean {
return plugin?.params?.dataset_id?.required || false;
}
export function getTestExtensions(plugin?: Plugin): string[] {
const results: string[] = [];
if (plugin?.data_sources) {
+1 -1
View File
@@ -38,7 +38,7 @@ heatmap:
version: 0.0.15
jupyterlite:
package: "@galaxyproject/jupyterlite"
version: 0.0.12
version: 0.0.16
kepler:
package: "@galaxyproject/kepler"
version: 0.0.3
@@ -72,15 +72,11 @@ class VisualizationsConfigParser:
log.info("Visualizations plugin disabled: %s. Skipping...", returned["name"])
return None
# record the embeddable flag - defaults to False
returned["embeddable"] = False
if "embeddable" in xml_tree.attrib:
returned["embeddable"] = asbool(xml_tree.attrib.get("embeddable"))
# record the visible flag - defaults to False
returned["hidden"] = False
if "hidden" in xml_tree.attrib:
returned["hidden"] = asbool(xml_tree.attrib.get("hidden"))
# record boolean flags - defaults to False
for keyword in ["embeddable", "hidden"]:
returned[keyword] = False
if keyword in xml_tree.attrib:
returned[keyword] = asbool(xml_tree.attrib.get(keyword))
# a (for now) text description of what the visualization does
description = xml_tree.find("description")
@@ -116,6 +116,7 @@ class VisualizationPlugin(ServesTemplatesPluginMixin):
"tags": self.config.get("tags"),
"title": self.config.get("title"),
"target": self.config.get("render_target", "galaxy_main"),
"params": self.config.get("params"),
"embeddable": self.config.get("embeddable"),
"entry_point": self.config.get("entry_point"),
"settings": self.config.get("settings"),