mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge remote-tracking branch 'upstream/release_23.0' into dev
This commit is contained in:
Vendored
+1
-1
@@ -1 +1 @@
|
||||
GALAXY_TEST_TOOL_CONF="lib/galaxy/config/sample/tool_conf.xml.sample,test/functional/tools/samples_tool_conf.xml"
|
||||
GALAXY_TEST_TOOL_CONF="lib/galaxy/config/sample/tool_conf.xml.sample,test/functional/tools/sample_tool_conf.xml"
|
||||
|
||||
Vendored
+1
-1
@@ -10,7 +10,7 @@
|
||||
"env": {
|
||||
"GALAXY_CONFIG_FILE": "${workspaceFolder}/config/galaxy.yml",
|
||||
"GALAXY_CONDA_AUTO_INIT": "false",
|
||||
"GALAXY_CONFIG_TOOL_CONFIG_FILE": "lib/galaxy/config/sample/tool_conf.xml.sample,test/functional/tools/samples_tool_conf.xml",
|
||||
"GALAXY_CONFIG_TOOL_CONFIG_FILE": "lib/galaxy/config/sample/tool_conf.xml.sample,test/functional/tools/sample_tool_conf.xml",
|
||||
"GALAXY_CONFIG_DATABASE_CONNECTION": "postgresql://localhost/galaxy"
|
||||
}
|
||||
},
|
||||
|
||||
+46
-49
@@ -1,19 +1,19 @@
|
||||
# Client Build System
|
||||
|
||||
Installs, stages, and builds the client-side scripts necessary for running the
|
||||
Galaxy webapp. When started through `run.sh` or any other method that utilizes
|
||||
`scripts/common_startup.sh`, Galaxy will (since 18.09) _automatically_ build
|
||||
the client as a part of server startup, when it detects changes, unless that
|
||||
Galaxy web client. When started through `run.sh` or any other method that
|
||||
utilizes `scripts/common_startup.sh`, Galaxy will (since 18.09) _automatically_
|
||||
build the client as a part of server startup when it detects changes unless that
|
||||
functionality is explicitly disabled.
|
||||
|
||||
The base dependencies used are Node.js and Yarn. Galaxy now includes these in
|
||||
the virtual environment, and they can be accessed by activating that with `.
|
||||
.venv/bin/activate` from the Galaxy root directory.
|
||||
The base dependencies used are Node.js and Yarn. Galaxy includes appropriate
|
||||
versions of these in the virtual environment, and they can be accessed by
|
||||
activating that with `. .venv/bin/activate` from the Galaxy root directory.
|
||||
|
||||
If you'd like to install your own dependencies, on OSX the easiest way to get
|
||||
set up is using `homebrew` and the command `brew install nodejs yarn`. More
|
||||
information, including instructions for other platforms, is available at
|
||||
[https://nodejs.org](https://nodejs.org) and
|
||||
If you'd like to install your dependencies external to Galaxy, on OSX the
|
||||
easiest way to get set up is using `homebrew` and the command `brew install
|
||||
nodejs yarn`. More information, including instructions for other platforms, is
|
||||
available at [https://nodejs.org](https://nodejs.org) and
|
||||
[https://yarnpkg.com/](https://yarnpkg.com).
|
||||
|
||||
The Galaxy client build has necessarily grown more complex in the past several
|
||||
@@ -27,11 +27,11 @@ directly on Gitter at
|
||||
|
||||
There are many moving parts to the client build system, but the entry point for
|
||||
most people is the 'client' rule in the Makefile at the root of the Galaxy
|
||||
repository. Execute the following to perform a complete build suitable for
|
||||
local development, including dependency staging, style building, script
|
||||
processing, and bundling. This is a development-specific build which includes
|
||||
extra debugging features, and excludes several production optimizations made
|
||||
during the build process.
|
||||
repository. Execute the following to perform a complete build suitable for local
|
||||
development, including dependency staging, style building, script processing,
|
||||
and bundling. This is a development-specific build that includes extra debugging
|
||||
features and excludes several production optimizations made during the build
|
||||
process.
|
||||
|
||||
make client
|
||||
|
||||
@@ -53,39 +53,31 @@ and these should not be committed.
|
||||
When you're actively developing, it is convenient to have the client
|
||||
automatically rebuild every time you save a file. You can do this using:
|
||||
|
||||
make client-watch
|
||||
|
||||
This will first stage client dependencies, initiate a build, and then will
|
||||
watch for changes in any of the galaxy client source files. When a file is
|
||||
changed, the client will automatically rebuild, after which you can refresh
|
||||
your browser to see changes.
|
||||
|
||||
For even more rapid development you can use the webpack development server,
|
||||
which takes advantage of hot module replacement (HMR). This technique allows
|
||||
swapping out of javascript modules while the application is running without
|
||||
requiring a full page reload most of the time, at least in the more modern
|
||||
parts of the application.
|
||||
|
||||
The command below starts a special webpack dev server after a client
|
||||
build.
|
||||
|
||||
make client-dev-server
|
||||
|
||||
This will start up an extra client development server running on port 8081.
|
||||
Open your browser to http://localhost:8081 (instead of the default 8080 that
|
||||
Galaxy would run on), and you should see Galaxy like normal. Except now, when
|
||||
you change client code it'll automatically rebuild *and* reload the relevant
|
||||
portion of the application for you. Lastly, if you
|
||||
are running Galaxy at a location other than the default, you can specify a
|
||||
different proxy target (in this example, port 8000) using the GALAXY_URL
|
||||
environment variable:
|
||||
Or, with the package scripts from this `client` directory:
|
||||
|
||||
yarn run develop
|
||||
|
||||
This will start up an extra client development server running on port 8081. Open
|
||||
your browser to `http://localhost:8081` (instead of the default 8080 that Galaxy
|
||||
would run on), and you should see Galaxy like normal. Except now, when you
|
||||
change client code it'll automatically rebuild _and_ reload the relevant portion
|
||||
of the application for you. Lastly, if you are running Galaxy at a location
|
||||
other than the default, you can specify a different proxy target (in this
|
||||
example, port 8000) using the GALAXY_URL environment variable:
|
||||
|
||||
GALAXY_URL="http://localhost:8000" make client-dev-server
|
||||
|
||||
Sometimes you want to run your local UI against a remote Galaxy server. This is also possible, if you enable `CHANGE_ORIGIN` flag
|
||||
Sometimes you want to run your local UI against a remote Galaxy server. This is
|
||||
also possible if you set the `CHANGE_ORIGIN` environment variable:
|
||||
|
||||
CHANGE_ORIGIN=true GALAXY_URL="https://usegalaxy.org/" make client-dev-server
|
||||
|
||||
You can also specify a particular port to bind the dev server to:
|
||||
|
||||
WEBPACK_PORT=8083 yarn run develop
|
||||
|
||||
## Running a Separate Server
|
||||
|
||||
When developing the client it can be helpful to run a local server for the
|
||||
@@ -94,17 +86,22 @@ commands. This command will run galaxy without building the client:
|
||||
|
||||
make skip-client
|
||||
|
||||
Or by setting the following environment variable and running Galaxy however you
|
||||
prefer:
|
||||
|
||||
GALAXY_SKIP_CLIENT_BUILD=1 ./run.sh
|
||||
|
||||
## Changing Styles/CSS
|
||||
|
||||
Galaxy uses Sass for its styling, which is a superset of CSS that compiles down
|
||||
to regular CSS. Most Galaxy styling source (.scss) files are kept in
|
||||
`client/src/style/scss`. There are additionally style blocks alongside some Vue
|
||||
components -- styles that are particular to that individual component and do
|
||||
not apply site-wide.
|
||||
Galaxy uses Sass for globally applied styling, which is a superset of CSS that
|
||||
compiles down to regular CSS. Most Galaxy styling source (.scss) files are kept
|
||||
in `client/src/style/scss`. Many components will also have local style blocks
|
||||
containing styles that are particular to that individual component and do not
|
||||
apply site-wide.
|
||||
|
||||
On build, the compiled css bundle is served at `/static/style/base.css`.
|
||||
|
||||
As mentioned above, `make client` will rebuild styles, as a part of the webpack
|
||||
As mentioned above, `make client` will rebuild styles as a part of the webpack
|
||||
build. For iterative development, "Watch Mode" rebuilds as described above do
|
||||
include style changes.
|
||||
|
||||
@@ -142,8 +139,8 @@ directory. This is what happens during a complete client build.
|
||||
|
||||
During client-side development, it is more convenient to have granular testing
|
||||
options. The various testing scripts are defined inside package.json within the
|
||||
client folder, and are called with `yarn` as demonstrated in the
|
||||
following commands.
|
||||
client folder and are called with `yarn` as demonstrated in the following
|
||||
commands.
|
||||
|
||||
This is what CI is going to run, and also what 'make client-test' invokes,
|
||||
executing all the client tests:
|
||||
@@ -154,7 +151,7 @@ You can also bypass qunit and single-run all of the jest tests like so:
|
||||
|
||||
yarn run jest
|
||||
|
||||
Or, if you really want to run just the qunit tests:
|
||||
Or, if you want to run just the qunit tests:
|
||||
|
||||
yarn run qunit
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
<div class="dropdown-menu" aria-labelledby="dataset-dropdown">
|
||||
<a class="dropdown-item" href="#" @click.prevent="showDataset">
|
||||
<span class="fa fa-eye fa-fw mr-1" />
|
||||
<span>Show in History</span>
|
||||
<span>Switch to History containing dataset</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="copyDataset">
|
||||
<span class="fa fa-copy fa-fw mr-1" />
|
||||
<span>Copy to History</span>
|
||||
<span>Copy to current History</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,16 +5,8 @@
|
||||
<script>
|
||||
import HistoryList from "./history-list";
|
||||
export default {
|
||||
props: {
|
||||
actionId: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
new HistoryList.View({
|
||||
action_id: this.actionId,
|
||||
}).$el.appendTo(this.$refs.target);
|
||||
new HistoryList.View().$el.appendTo(this.$refs.target);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -62,15 +62,10 @@ var View = Backbone.View.extend({
|
||||
initialize: function (options) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
LoadingIndicator.markViewAsLoading(this);
|
||||
|
||||
if (options.action_id == "list_published") {
|
||||
this.active_tab = "shared";
|
||||
} else if (options.action_id == "list") {
|
||||
this.active_tab = "user";
|
||||
}
|
||||
this.active_tab = "user";
|
||||
this.model = new Backbone.Model();
|
||||
Utils.get({
|
||||
url: `${getAppRoot()}history/${options.action_id}?${$.param(Galaxy.params)}`,
|
||||
url: `${getAppRoot()}history/list?${$.param(Galaxy.params)}`,
|
||||
success: (response) => {
|
||||
this.model.set(response);
|
||||
this.render();
|
||||
|
||||
@@ -33,6 +33,7 @@ export const STATES = {
|
||||
/** metadata discovery/setting failed or errored (but otherwise ok) */
|
||||
failed_metadata: {
|
||||
status: "danger",
|
||||
text: "Metadata generation failed. Please retry.",
|
||||
icon: "exclamation-triangle",
|
||||
},
|
||||
/** was created without a tool */
|
||||
|
||||
@@ -137,9 +137,19 @@ export default {
|
||||
this.config.toolbox_auto_sort === true &&
|
||||
!this.category.elems.some((el) => el.text !== undefined && el.text !== "")
|
||||
) {
|
||||
return Object.entries(
|
||||
[...this.category.elems].sort((a, b) => a.name.toLowerCase() > b.name.toLowerCase())
|
||||
);
|
||||
const elements = [...this.category.elems];
|
||||
const sorted = elements.sort((a, b) => {
|
||||
const aNameLower = a.name.toLowerCase();
|
||||
const bNameLower = b.name.toLowerCase();
|
||||
if (aNameLower > bNameLower) {
|
||||
return 1;
|
||||
} else if (aNameLower < bNameLower) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
return Object.entries(sorted);
|
||||
} else {
|
||||
return Object.entries(this.category.elems);
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
disabled: false,
|
||||
initialized: false,
|
||||
showLoading: true,
|
||||
showForm: false,
|
||||
showEntryPoints: false,
|
||||
@@ -225,7 +226,7 @@ export default {
|
||||
watch: {
|
||||
getLatestCreateTime() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy && Galaxy.currHistoryPanel) {
|
||||
if (this.initialized && Galaxy && Galaxy.currHistoryPanel) {
|
||||
console.debug("History change watcher detected a change.");
|
||||
this.onHistoryChange();
|
||||
}
|
||||
@@ -233,6 +234,7 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.requestTool().then(() => {
|
||||
this.initialized = true;
|
||||
console.debug(`ToolForm::created - Started listening to history changes. [${this.id}]`);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -236,7 +236,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
setup(props, { emit }) {
|
||||
const { datatypes, datatypesMapper, datatypesMapperLoading } = useDatatypesMapper();
|
||||
const connectionsStore = useConnectionStore();
|
||||
const stepStore = useWorkflowStepStore();
|
||||
@@ -260,6 +260,7 @@ export default {
|
||||
}
|
||||
onUnmounted(() => {
|
||||
resetStores();
|
||||
emit("update:confirmation", false);
|
||||
});
|
||||
return {
|
||||
connectionsStore,
|
||||
|
||||
@@ -23,7 +23,11 @@ const props = defineProps<{
|
||||
const stepStore = useWorkflowStepStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
const outputIsMappedOver = computed(() => stepStore.stepMapOver[props.connection.output.stepId]?.isCollection);
|
||||
const inputIsMappedOver = computed(() => stepStore.stepMapOver[props.connection.input.stepId]?.isCollection);
|
||||
const inputIsMappedOver = computed(
|
||||
() =>
|
||||
stepStore.stepInputMapOver[props.connection.input.stepId] &&
|
||||
stepStore.stepInputMapOver[props.connection.input.stepId][props.connection.input.name]?.isCollection
|
||||
);
|
||||
const outputIsOptional = computed(() => {
|
||||
return Boolean(
|
||||
stepStore.getStep(props.connection.output.stepId)?.when ||
|
||||
|
||||
@@ -48,9 +48,10 @@ import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
|
||||
import type { TerminalPosition } from "@/stores/workflowEditorStateStore";
|
||||
import { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import { useWorkflowStepStore, type Step } from "@/stores/workflowStepStore";
|
||||
import { useZoom } from "./composables/useZoom";
|
||||
import { useD3Zoom } from "./composables/d3Zoom";
|
||||
import type { XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
import type { OutputTerminals } from "./modules/terminals";
|
||||
import { minZoom, maxZoom } from "./modules/zoomLevels";
|
||||
|
||||
const emit = defineEmits(["transform", "graph-offset", "onRemove", "scrollTo"]);
|
||||
const props = defineProps({
|
||||
@@ -67,7 +68,7 @@ const canvas: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
const elementBounding = useElementBounding(canvas, { windowResize: false, windowScroll: false });
|
||||
const scroll = useScroll(canvas);
|
||||
const { transform, panBy, setZoom, moveTo } = useZoom(1, 0.2, 5, canvas, scroll);
|
||||
const { transform, panBy, setZoom, moveTo } = useD3Zoom(1, minZoom, maxZoom, canvas, scroll);
|
||||
|
||||
const isDragging = ref(false);
|
||||
provide("isDragging", isDragging);
|
||||
|
||||
@@ -1,8 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
import Vue, { computed } from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { getZoomInLevel, getZoomOutLevel, isMinZoom, isMaxZoom } from "./modules/zoomLevels";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
const props = defineProps({
|
||||
zoomLevel: { type: Number, default: 1 },
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onZoom", zoom: number): void;
|
||||
}>();
|
||||
|
||||
const zoomDefault = 1;
|
||||
const zoomPercentage = computed(() => Math.round(props.zoomLevel * 100));
|
||||
|
||||
function onZoomIn() {
|
||||
emit("onZoom", getZoomInLevel(props.zoomLevel));
|
||||
}
|
||||
|
||||
function onZoomOut() {
|
||||
emit("onZoom", getZoomOutLevel(props.zoomLevel));
|
||||
}
|
||||
|
||||
function onZoomReset() {
|
||||
emit("onZoom", zoomDefault);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="zoom-control float-right btn-group-horizontal">
|
||||
<b-button
|
||||
v-b-tooltip.hover
|
||||
:disabled="isMin"
|
||||
:disabled="isMinZoom(props.zoomLevel)"
|
||||
role="button"
|
||||
class="fa fa-minus"
|
||||
title="Zoom Out"
|
||||
@@ -21,8 +51,7 @@
|
||||
{{ zoomPercentage }}%
|
||||
</b-button>
|
||||
<b-button
|
||||
v-b-tooltip.hover
|
||||
:disabled="isMax"
|
||||
:disabled="isMaxZoom(props.zoomLevel)"
|
||||
role="button"
|
||||
class="fa fa-plus"
|
||||
title="Zoom In"
|
||||
@@ -32,54 +61,6 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Vue, { computed } from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
zoomLevel: number;
|
||||
}>(),
|
||||
{
|
||||
zoomLevel: 1,
|
||||
}
|
||||
);
|
||||
const emit = defineEmits(["onZoom"]);
|
||||
|
||||
const zoomDefault = 1;
|
||||
const zoomLevels = [0.1, 0.2, 0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.33, 1.5, 2, 2.5, 3, 4, 5];
|
||||
const isMin = computed(() => Math.round(props.zoomLevel * 100) == Math.round(zoomLevels[0] * 100));
|
||||
const isMax = computed(() => Math.round(props.zoomLevel * 100) == Math.round(zoomLevels.at(-1)! * 100));
|
||||
const zoomPercentage = computed(() => Math.round(props.zoomLevel * 100));
|
||||
const index = computed(() => {
|
||||
let index = zoomLevels.indexOf(props.zoomLevel);
|
||||
if (index < 0) {
|
||||
const closest = zoomLevels.reduce((prev, curr) => {
|
||||
return Math.abs(curr - props.zoomLevel) < Math.abs(prev - props.zoomLevel) ? curr : prev;
|
||||
});
|
||||
index = zoomLevels.indexOf(closest);
|
||||
}
|
||||
return index;
|
||||
});
|
||||
function onZoomIn() {
|
||||
const zoomLevel = zoomLevels[index.value + 1];
|
||||
if (zoomLevel) {
|
||||
emit("onZoom", zoomLevel);
|
||||
}
|
||||
}
|
||||
function onZoomOut() {
|
||||
const zoomLevel = zoomLevels[index.value - 1];
|
||||
if (zoomLevel) {
|
||||
emit("onZoom", zoomLevel);
|
||||
}
|
||||
}
|
||||
function onZoomReset() {
|
||||
emit("onZoom", zoomDefault);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zoom-reset {
|
||||
width: 4rem;
|
||||
|
||||
+2
-16
@@ -1,6 +1,6 @@
|
||||
import { zoom, zoomIdentity, type D3ZoomEvent } from "d3-zoom";
|
||||
import { ref, watch } from "vue";
|
||||
import { pointer, select } from "d3-selection";
|
||||
import { select } from "d3-selection";
|
||||
import type { Ref } from "vue";
|
||||
import type { XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
import type { UseScrollReturn } from "@vueuse/core";
|
||||
@@ -12,7 +12,7 @@ const filter = (event: any) => {
|
||||
return !preventZoom;
|
||||
};
|
||||
|
||||
export function useZoom(
|
||||
export function useD3Zoom(
|
||||
k: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
@@ -29,20 +29,6 @@ export function useZoom(
|
||||
.translate(transform.value.x, transform.value.y)
|
||||
.scale(transform.value.k);
|
||||
d3Zoom.transform(d3Selection, updatedTransform);
|
||||
|
||||
d3Selection
|
||||
.on("wheel", (event) => {
|
||||
const currentZoom = d3Selection.property("__zoom").k || 1;
|
||||
|
||||
const pinchDelta =
|
||||
-event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10;
|
||||
const point = pointer(event);
|
||||
point[0] += scroll.x.value;
|
||||
point[1] += scroll.y.value;
|
||||
const zoom = currentZoom * 2 ** pinchDelta;
|
||||
d3Zoom.scaleTo(d3Selection, zoom, point);
|
||||
})
|
||||
.on("wheel.zoom", null);
|
||||
}
|
||||
|
||||
d3Zoom.on("zoom", (event: D3ZoomEvent<HTMLElement, unknown>) => {
|
||||
@@ -102,6 +102,9 @@ describe("canAccept", () => {
|
||||
dataIn.connect(collectionOut);
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
expect(dataIn.canAccept(collectionOut).canAccept).toBe(false);
|
||||
expect(dataIn.canAccept(collectionOut).reason).toBe(
|
||||
"Input already filled with another connection, delete it before connecting another output."
|
||||
);
|
||||
dataIn.disconnect(collectionOut);
|
||||
expect(dataIn.canAccept(collectionOut).canAccept).toBe(true);
|
||||
expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
@@ -205,6 +208,8 @@ describe("canAccept", () => {
|
||||
const integerInputParam = terminals["multi data"]["advanced|advanced_threshold"] as InputParameterTerminal;
|
||||
const integerOutputParam = terminals["integer parameter input"]["output"] as OutputParameterTerminal;
|
||||
expect(integerInputParam.canAccept(integerOutputParam).canAccept).toBe(true);
|
||||
// regression test for https://github.com/galaxyproject/galaxy/issues/15417
|
||||
expect(integerInputParam.canAccept(integerOutputParam).reason).toBe(null);
|
||||
});
|
||||
it("rejects text to integer parameter connection", () => {
|
||||
const integerInputParam = terminals["multi data"]["advanced|advanced_threshold"] as InputParameterTerminal;
|
||||
@@ -261,17 +266,58 @@ describe("canAccept", () => {
|
||||
dataIn.connect(collectionOut);
|
||||
dataInTwo.connect(dataOut);
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
expect(dataIn.localMapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
//
|
||||
dataIn.disconnect(collectionOut);
|
||||
// this is weird and not particularly robust, if you save and reload this will most likely not be constrained
|
||||
// TODO: avoid this if possible ...
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
expect(dataIn.localMapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
expect(dataIn.localMapOver.isCollection).toBe(true);
|
||||
expect(dataIn.canAccept(simpleDataOut).canAccept).toBe(false);
|
||||
expect(dataIn.canAccept(simpleDataOut).reason).toBe(
|
||||
"Cannot attach non-collection outputs to mapped over inputs, consider disconnecting inputs and outputs to reset this input's mapping."
|
||||
"Cannot attach non-collection output to mapped over input, consider disconnecting inputs and outputs to reset this input's mapping."
|
||||
);
|
||||
dataInTwo.disconnect(dataOut);
|
||||
// terminal isn't a reactive class in any way (worth a thought doing, but difficult!), the following happens when a new terminal is built
|
||||
// in useTerminal.ts
|
||||
const rebuiltDataIn = terminalFactory(
|
||||
dataIn.stepId,
|
||||
advancedSteps[dataIn.stepId].inputs[0],
|
||||
testDatatypesMapper
|
||||
);
|
||||
expect(rebuiltDataIn.localMapOver.isCollection).toBe(false);
|
||||
expect(rebuiltDataIn.canAccept(simpleDataOut).canAccept).toBe(true);
|
||||
});
|
||||
it("accepts attaching dataset to non-mapped over input for a mapped over step", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const simpleDataOut = terminals["data input"]["output"] as OutputTerminal;
|
||||
const otherDataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const multiSimpleInputOne = terminals["multiple simple data"]["input1"] as InputTerminal;
|
||||
const multiSimpleInputTwo = terminals["multiple simple data"]["queries_0|input2"] as InputTerminal;
|
||||
const multiSimpleOut = terminals["multiple simple data"]["out_file1"] as OutputTerminal;
|
||||
// map over simple input
|
||||
multiSimpleInputOne.connect(collectionOut);
|
||||
// constrain input by connecting an output to same step
|
||||
otherDataIn.connect(multiSimpleOut);
|
||||
// can connect a simple input to the non mapped-over input of the mapped-over step
|
||||
expect(multiSimpleInputTwo.canAccept(simpleDataOut).canAccept).toBe(true);
|
||||
// can also connect a connection of the same map over state
|
||||
expect(multiSimpleInputTwo.canAccept(collectionOut).canAccept).toBe(true);
|
||||
// cannot connect connection of deeper nesting
|
||||
const listListOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
expect(multiSimpleInputTwo.canAccept(listListOut).canAccept).toBe(false);
|
||||
expect(multiSimpleInputTwo.canAccept(listListOut).reason).toBe(
|
||||
"Can't map over this input with output collection type - other inputs have an incompatible map over collection type. Disconnect inputs (and potentially outputs) and retry."
|
||||
);
|
||||
// disconnect, map over should be maintained
|
||||
multiSimpleInputOne.disconnect(collectionOut);
|
||||
expect(multiSimpleInputOne.localMapOver.isCollection).toBe(true);
|
||||
expect(multiSimpleInputOne.canAccept(simpleDataOut).canAccept).toBe(false);
|
||||
expect(multiSimpleInputOne.canAccept(simpleDataOut).reason).toBe(
|
||||
"Cannot attach non-collection output to mapped over input, consider disconnecting inputs and outputs to reset this input's mapping."
|
||||
);
|
||||
});
|
||||
// TODO: test mapOver reset when constraint removed
|
||||
it("resets mapOver when constraint is lifted", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
TerminalSource,
|
||||
} from "@/stores/workflowStepStore";
|
||||
import type { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import Vue from "vue";
|
||||
|
||||
export class ConnectionAcceptable {
|
||||
reason: string | null;
|
||||
@@ -53,6 +54,7 @@ class Terminal extends EventEmitter {
|
||||
stepId: number;
|
||||
terminalType: "input" | "output";
|
||||
datatypesMapper: DatatypesMapperModel;
|
||||
localMapOver: CollectionTypeDescriptor;
|
||||
|
||||
constructor(attr: BaseTerminalArgs) {
|
||||
super();
|
||||
@@ -63,6 +65,7 @@ class Terminal extends EventEmitter {
|
||||
this.multiple = false;
|
||||
this.terminalType = "input";
|
||||
this.datatypesMapper = attr.datatypesMapper;
|
||||
this.localMapOver = NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
}
|
||||
public get id() {
|
||||
return `node-${this.stepId}-${this.terminalType}-${this.name}`;
|
||||
@@ -105,10 +108,12 @@ class Terminal extends EventEmitter {
|
||||
outputVal = val.effectiveMapOver(description);
|
||||
}
|
||||
const effectiveMapOver = this._effectiveMapOver(outputVal);
|
||||
if (!this.localMapOver.equal(effectiveMapOver)) {
|
||||
this.stepStore.changeStepInputMapOver(this.stepId, this.name, effectiveMapOver);
|
||||
this.localMapOver = effectiveMapOver;
|
||||
}
|
||||
if (!this.mapOver.equal(effectiveMapOver)) {
|
||||
this.stepStore.changeStepMapOver(this.stepId, effectiveMapOver);
|
||||
// TODO: do we even need to listen for this?
|
||||
this.emit("changeMapOver", outputVal);
|
||||
}
|
||||
}
|
||||
_effectiveMapOver(otherCollectionType: CollectionTypeDescriptor) {
|
||||
@@ -119,8 +124,7 @@ class Terminal extends EventEmitter {
|
||||
}
|
||||
resetMapping(connection?: Connection) {
|
||||
this.stepStore.changeStepMapOver(this.stepId, NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
// necessary ?
|
||||
this.emit("changeMapOver", this.mapOver);
|
||||
this.stepStore.resetStepInputMapOver(this.stepId);
|
||||
}
|
||||
hasConnectedMappedInputTerminals() {
|
||||
// check if step has connected and mapped input terminals ... should maybe be on step/node ?
|
||||
@@ -150,7 +154,7 @@ class Terminal extends EventEmitter {
|
||||
// No output terminals are counting on this being mapped
|
||||
// over if connected inputs are still mapped over or if none
|
||||
// of the outputs are connected...
|
||||
const reset = this.hasConnectedMappedInputTerminals() || !this.hasConnectedOutputTerminals();
|
||||
const reset = !this.hasConnectedOutputTerminals();
|
||||
if (reset) {
|
||||
this.resetMapping(connection);
|
||||
}
|
||||
@@ -160,12 +164,18 @@ class Terminal extends EventEmitter {
|
||||
class BaseInputTerminal extends Terminal {
|
||||
datatypes: InputTerminalInputs["datatypes"];
|
||||
optional: InputTerminalInputs["optional"];
|
||||
localMapOver: CollectionTypeDescriptor;
|
||||
|
||||
constructor(attr: InputTerminalArgs) {
|
||||
super(attr);
|
||||
this.datatypes = attr.input.datatypes;
|
||||
this.multiple = attr.input.multiple;
|
||||
this.optional = attr.input.optional;
|
||||
if (this.stepStore.stepInputMapOver[this.stepId] && this.stepStore.stepInputMapOver[this.stepId][this.name]) {
|
||||
this.localMapOver = this.stepStore.stepInputMapOver[this.stepId][this.name];
|
||||
} else {
|
||||
this.localMapOver = NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
}
|
||||
}
|
||||
connect(other: BaseOutputTerminal): void {
|
||||
super.connect(other);
|
||||
@@ -174,9 +184,15 @@ class BaseInputTerminal extends Terminal {
|
||||
setDefaultMapOver(other: BaseOutputTerminal) {
|
||||
const otherCollectionType = this._otherCollectionType(other);
|
||||
if (otherCollectionType.isCollection) {
|
||||
this.setMapOver(otherCollectionType);
|
||||
return this.setMapOver(otherCollectionType);
|
||||
}
|
||||
}
|
||||
getStepMapOver() {
|
||||
this.getConnectedTerminals().forEach((other) => {
|
||||
this.setDefaultMapOver(other);
|
||||
});
|
||||
}
|
||||
|
||||
canAccept(outputTerminal: BaseOutputTerminal) {
|
||||
if (this.stepId == outputTerminal.stepId) {
|
||||
return new ConnectionAcceptable(false, "Cannot connect output to input of same step.");
|
||||
@@ -201,6 +217,7 @@ class BaseInputTerminal extends Terminal {
|
||||
}
|
||||
resetMapping(connection?: Connection) {
|
||||
super.resetMapping(connection);
|
||||
this.stepStore.changeStepInputMapOver(this.stepId, this.name, NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
const outputStepIds = this._getOutputTerminals().map((outputTerminal) => outputTerminal.stepId);
|
||||
if (connection) {
|
||||
outputStepIds.push(connection.output.stepId);
|
||||
@@ -360,14 +377,16 @@ interface InvalidInputTerminalArgs extends InputTerminalArgs {
|
||||
|
||||
export class InvalidInputTerminal extends BaseInputTerminal {
|
||||
valid: false;
|
||||
localMapOver: CollectionTypeDescriptor;
|
||||
|
||||
constructor(attr: InvalidInputTerminalArgs) {
|
||||
super(attr);
|
||||
this.valid = false;
|
||||
this.localMapOver = NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
}
|
||||
|
||||
attachable(terminal: BaseOutputTerminal) {
|
||||
return new ConnectionAcceptable(false, "Cannot attach to invalid input.");
|
||||
return new ConnectionAcceptable(false, "Cannot attach to invalid input. Disconnect this input.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,7 +397,11 @@ export class InputTerminal extends BaseInputTerminal {
|
||||
constructor(attr: InputTerminalArgs) {
|
||||
super(attr);
|
||||
this.collection = false;
|
||||
this.getConnectedTerminals().map((other) => this.setDefaultMapOver(other));
|
||||
this.getStepMapOver();
|
||||
}
|
||||
|
||||
isMappedOver(): boolean {
|
||||
return Boolean(this.localMapOver.isCollection);
|
||||
}
|
||||
|
||||
attachable(other: BaseOutputTerminal) {
|
||||
@@ -435,11 +458,13 @@ export class InputTerminal extends BaseInputTerminal {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (mapOver.isCollection) {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Cannot attach non-collection outputs to mapped over inputs, consider disconnecting inputs and outputs to reset this input's mapping."
|
||||
);
|
||||
} else {
|
||||
if (this.localMapOver.isCollection) {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
"Cannot attach non-collection output to mapped over input, consider disconnecting inputs and outputs to reset this input's mapping."
|
||||
);
|
||||
}
|
||||
}
|
||||
return this._producesAcceptableDatatypeAndOptionalness(other);
|
||||
}
|
||||
@@ -455,7 +480,7 @@ export class InputParameterTerminal extends BaseInputTerminal {
|
||||
constructor(attr: InputParameterTerminalArgs) {
|
||||
super(attr);
|
||||
this.type = attr.type;
|
||||
this.getConnectedTerminals().map((other) => this.setDefaultMapOver(other));
|
||||
this.getStepMapOver();
|
||||
}
|
||||
|
||||
effectiveType(parameterType: string) {
|
||||
@@ -465,9 +490,10 @@ export class InputParameterTerminal extends BaseInputTerminal {
|
||||
const effectiveThisType = this.effectiveType(this.type);
|
||||
const otherType = ("type" in other && other.type) || "data";
|
||||
const effectiveOtherType = this.effectiveType(otherType);
|
||||
const canAccept = effectiveThisType === effectiveOtherType
|
||||
return new ConnectionAcceptable(
|
||||
effectiveThisType == effectiveOtherType,
|
||||
`Cannot attach a ${effectiveOtherType} parameter to a ${effectiveThisType} input`
|
||||
canAccept,
|
||||
canAccept ? null: `Cannot attach a ${effectiveOtherType} parameter to a ${effectiveThisType} input`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -490,7 +516,7 @@ export class InputCollectionTerminal extends BaseInputTerminal {
|
||||
if (!this.collectionTypes.length) {
|
||||
this.collectionTypes.push(ANY_COLLECTION_TYPE_DESCRIPTION);
|
||||
}
|
||||
this.getConnectedTerminals().map((other) => this.setDefaultMapOver(other));
|
||||
this.getStepMapOver();
|
||||
}
|
||||
_effectiveMapOver(otherCollectionType: CollectionTypeDescriptor) {
|
||||
const collectionTypes = this.collectionTypes;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
export const zoomLevels = [
|
||||
0.1, 0.2, 0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.33, 1.5, 2, 2.5, 3, 4, 5,
|
||||
] as const;
|
||||
|
||||
export type ZoomLevel = (typeof zoomLevels)[number];
|
||||
|
||||
export const minZoom = zoomLevels[0];
|
||||
export const maxZoom = zoomLevels[zoomLevels.length - 1];
|
||||
|
||||
/**
|
||||
* Finds the closest snapped zoom level
|
||||
* @param zoom decimal number indicating current zoom multiplier
|
||||
* @returns snapped zoom level
|
||||
*/
|
||||
export function getSnappedZoom(zoom: number): ZoomLevel {
|
||||
return zoomLevels.reduce((a, b) => {
|
||||
return Math.abs(b - zoom) < Math.abs(a - zoom) ? b : a;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the next largest zoom level
|
||||
* @param zoom decimal number indicating current zoom multiplier
|
||||
* @returns snapped zoom level
|
||||
*/
|
||||
export function getZoomInLevel(zoom: number): ZoomLevel {
|
||||
const snapped = getSnappedZoom(zoom);
|
||||
const index = zoomLevels.indexOf(snapped);
|
||||
|
||||
if (index === zoomLevels.length - 1) {
|
||||
return snapped;
|
||||
} else {
|
||||
return zoomLevels[index + 1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next smaller zoom level
|
||||
* @param zoom decimal number indicating current zoom multiplier
|
||||
* @returns snapped zoom level
|
||||
*/
|
||||
export function getZoomOutLevel(zoom: number): ZoomLevel {
|
||||
const snapped = getSnappedZoom(zoom);
|
||||
const index = zoomLevels.indexOf(snapped);
|
||||
|
||||
if (index === 0) {
|
||||
return snapped;
|
||||
} else {
|
||||
return zoomLevels[index - 1];
|
||||
}
|
||||
}
|
||||
|
||||
export function isMinZoom(zoom: number): boolean {
|
||||
return zoom === minZoom;
|
||||
}
|
||||
|
||||
export function isMaxZoom(zoom: number): boolean {
|
||||
return zoom === maxZoom;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export function useDetailedDatatypes() {
|
||||
|
||||
async function getDatatypes() {
|
||||
try {
|
||||
const datatypesPromise = datatypesFetcher({ extension_only: true });
|
||||
const datatypesPromise = datatypesFetcher({ extension_only: false });
|
||||
const datatypeEDAMFormatsPromise = edamFormatsFetcher({});
|
||||
const datatypeEDAMDataPromise = edamDataFetcher({});
|
||||
|
||||
|
||||
@@ -235,6 +235,12 @@ export function getRouter(Galaxy) {
|
||||
component: HistoryPublishedList,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "histories/list",
|
||||
component: GridHistory,
|
||||
props: true,
|
||||
redirect: redirectAnon(),
|
||||
},
|
||||
{
|
||||
path: "histories/:historyId/export",
|
||||
get component() {
|
||||
@@ -242,11 +248,6 @@ export function getRouter(Galaxy) {
|
||||
},
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "histories/:actionId",
|
||||
component: GridHistory,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "interactivetool_entry_points/list",
|
||||
component: InteractiveTools,
|
||||
|
||||
@@ -124,7 +124,7 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
|
||||
Vue.set(this.invalidConnections, connectionId, reason);
|
||||
},
|
||||
dropFromInvalidConnections(this: State, connectionId: string) {
|
||||
this.invalidConnections[connectionId] = undefined;
|
||||
Vue.delete(this.invalidConnections, connectionId);
|
||||
},
|
||||
removeConnection(this: State, terminal: InputTerminal | OutputTerminal | Connection["id"]) {
|
||||
const stepStore = useWorkflowStepStore();
|
||||
|
||||
@@ -8,6 +8,7 @@ interface State {
|
||||
steps: { [index: string]: Step };
|
||||
stepIndex: number;
|
||||
stepMapOver: { [index: number]: CollectionTypeDescriptor };
|
||||
stepInputMapOver: StepInputMapOver;
|
||||
}
|
||||
|
||||
interface StepPosition {
|
||||
@@ -133,10 +134,15 @@ interface WorkflowOutputs {
|
||||
};
|
||||
}
|
||||
|
||||
interface StepInputMapOver {
|
||||
[index: number]: { [index: string]: CollectionTypeDescriptor };
|
||||
}
|
||||
|
||||
export const useWorkflowStepStore = defineStore("workflowStepStore", {
|
||||
state: (): State => ({
|
||||
steps: {} as Steps,
|
||||
stepMapOver: {} as { [index: number]: CollectionTypeDescriptor },
|
||||
stepInputMapOver: {} as StepInputMapOver,
|
||||
stepIndex: -1,
|
||||
}),
|
||||
getters: {
|
||||
@@ -211,7 +217,17 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
|
||||
this.steps[step.id.toString()] = step;
|
||||
},
|
||||
changeStepMapOver(stepId: number, mapOver: CollectionTypeDescriptor) {
|
||||
this.stepMapOver[stepId] = mapOver;
|
||||
Vue.set(this.stepMapOver, stepId, mapOver);
|
||||
},
|
||||
resetStepInputMapOver(stepId: number) {
|
||||
Vue.set(this.stepInputMapOver, stepId, {});
|
||||
},
|
||||
changeStepInputMapOver(stepId: number, inputName: string, mapOver: CollectionTypeDescriptor) {
|
||||
if (this.stepInputMapOver[stepId]) {
|
||||
Vue.set(this.stepInputMapOver[stepId], inputName, mapOver);
|
||||
} else {
|
||||
Vue.set(this.stepInputMapOver, stepId, { [inputName]: mapOver });
|
||||
}
|
||||
},
|
||||
addConnection(connection: Connection) {
|
||||
const inputStep = this.getStep(connection.input.stepId);
|
||||
|
||||
@@ -250,16 +250,20 @@ module.exports = (env = {}, argv = {}) => {
|
||||
allowedHosts: process.env.GITPOD_WORKSPACE_ID ? "all" : "auto",
|
||||
devMiddleware: {
|
||||
publicPath: "/static/dist",
|
||||
writeToDisk: true,
|
||||
},
|
||||
hot: true,
|
||||
port: 8081,
|
||||
port: process.env.WEBPACK_PORT || 8081,
|
||||
host: "0.0.0.0",
|
||||
// proxy *everything* to the galaxy server.
|
||||
// someday, when we have a fully API-driven independent client, this
|
||||
// can be a more limited set -- e.g. `/api`, `/auth`
|
||||
proxy: {
|
||||
"**": {
|
||||
target: process.env.GALAXY_URL || "http://localhost:8080",
|
||||
// We explicitly use ipv4 loopback instead of localhost to
|
||||
// avoid ipv6/ipv4 resolution order issues; this should
|
||||
// align with Galaxy's default.
|
||||
target: process.env.GALAXY_URL || "http://127.0.0.1:8080",
|
||||
secure: process.env.CHANGE_ORIGIN ? !process.env.CHANGE_ORIGIN : true,
|
||||
changeOrigin: !!process.env.CHANGE_ORIGIN,
|
||||
logLevel: "debug",
|
||||
|
||||
@@ -75,8 +75,8 @@ Run a specific selenium test (under Linux or Mac OS X after installing geckodriv
|
||||
|
||||
Run a selenium test against a running server while watching client (fastest iterating on client tests):
|
||||
./run.sh & # run Galaxy on 8080
|
||||
make client-watch & # watch for client changes
|
||||
export GALAXY_TEST_EXTERNAL=http://localhost:8080/ # Target tests at server.
|
||||
make client-dev-server & # watch for client changes
|
||||
export GALAXY_TEST_EXTERNAL=http://localhost:8081/ # Target tests at server.
|
||||
. .venv/bin/activate # source the virtualenv so can skip run_tests.sh.
|
||||
pytest lib/galaxy_test/selenium/test_workflow_editor.py::TestWorkflowEditor::test_data_input
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import packaging.version
|
||||
|
||||
from galaxy.util import (
|
||||
commands,
|
||||
download_to_file,
|
||||
listify,
|
||||
shlex_join,
|
||||
smart_str,
|
||||
@@ -483,16 +484,13 @@ def hash_conda_packages(conda_packages: Iterable[CondaTarget]) -> str:
|
||||
def install_conda(conda_context: CondaContext, force_conda_build: bool = False) -> int:
|
||||
with tempfile.NamedTemporaryFile(suffix=".sh", prefix="conda_install", delete=False) as temp:
|
||||
script_path = temp.name
|
||||
download_cmd = commands.download_command(conda_link(), to=script_path)
|
||||
install_cmd = ["bash", script_path, "-b", "-p", conda_context.conda_prefix]
|
||||
package_targets = list(CONDA_PACKAGE_SPECS)
|
||||
if force_conda_build or conda_context.use_local:
|
||||
package_targets.extend(CONDA_BUILD_SPECS)
|
||||
log.info("Installing conda, this may take several minutes.")
|
||||
try:
|
||||
exit_code = conda_context.shell_exec(download_cmd)
|
||||
if exit_code:
|
||||
return exit_code
|
||||
download_to_file(conda_link(), script_path)
|
||||
exit_code = conda_context.shell_exec(install_cmd)
|
||||
except Exception:
|
||||
log.exception("Failed to install conda")
|
||||
|
||||
@@ -29,6 +29,7 @@ from galaxy.tool_util.deps.conda_util import (
|
||||
from galaxy.tool_util.deps.docker_util import command_list as docker_command_list
|
||||
from galaxy.util import (
|
||||
commands,
|
||||
download_to_file,
|
||||
safe_makedirs,
|
||||
shlex_join,
|
||||
unicodify,
|
||||
@@ -428,10 +429,12 @@ def ensure_installed(involucro_context, auto_init):
|
||||
def install_involucro(involucro_context):
|
||||
install_path = os.path.abspath(involucro_context.involucro_bin)
|
||||
involucro_context.involucro_bin = install_path
|
||||
download_cmd = commands.download_command(involucro_link(), to=install_path)
|
||||
exit_code = involucro_context.shell_exec(download_cmd)
|
||||
if exit_code:
|
||||
return exit_code
|
||||
|
||||
try:
|
||||
download_to_file(involucro_link(), install_path)
|
||||
except Exception:
|
||||
log.exception(f"Failed to download involucro from url '{involucro_link()}'")
|
||||
return 1
|
||||
try:
|
||||
os.chmod(install_path, os.stat(install_path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
return 0
|
||||
|
||||
@@ -706,14 +706,11 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi
|
||||
)
|
||||
# Index page with message
|
||||
workflow_id = trans.security.encode_id(stored_workflow.id)
|
||||
edit_url = url_for(f"/workflows/edit?id={workflow_id}")
|
||||
run_url = url_for(f"/workflows/run?id={workflow_id}")
|
||||
return trans.show_message(
|
||||
'Workflow "%s" created from current history. '
|
||||
'You can <a href="%s" target="_parent">edit</a> or <a href="%s" target="_parent">run</a> the workflow.'
|
||||
% (
|
||||
escape(workflow_name),
|
||||
url_for(controller="workflow", action="editor", id=workflow_id),
|
||||
url_for(controller="workflows", action="run", id=workflow_id),
|
||||
)
|
||||
f'Workflow "{escape(workflow_name)}" created from current history. '
|
||||
f'You can <a href="{edit_url}" target="_parent">edit</a> or <a href="{run_url}" target="_parent">run</a> the workflow.'
|
||||
)
|
||||
|
||||
def get_item(self, trans, id):
|
||||
|
||||
@@ -558,6 +558,17 @@ steps:
|
||||
editor.select_datatype(datatype="bam").wait_for_and_click()
|
||||
editor.node.output_data_row(output_name="out_file1", extension="bam").wait_for_visible()
|
||||
self.assert_connection_invalid("create_2#out_file1", "checksum#input")
|
||||
save_button = self.components.workflow_editor.save_button
|
||||
# Assert save button is disabled
|
||||
assert save_button.has_class("disabled")
|
||||
# Make connection valid again
|
||||
editor.change_datatype.wait_for_and_click()
|
||||
editor.select_datatype_text_search.wait_for_and_send_keys("tabular")
|
||||
editor.select_datatype(datatype="tabular").wait_for_and_click()
|
||||
# Assert connection is valid
|
||||
self.assert_connected("create_2#out_file1", "checksum#input")
|
||||
# Assert save button is enabled
|
||||
assert not save_button.has_class("disabled")
|
||||
|
||||
@selenium_test
|
||||
def test_change_datatype_post_job_action_lost_regression(self):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<tool id="interactive_tool_ml_jupyter_notebook" tool_type="interactive" name="GPU enabled Interactive Jupyter Notebook for Machine Learning" version="@VERSION@" profile="22.01">
|
||||
<macros>
|
||||
<token name="@VERSION@">0.1</token>
|
||||
<token name="@VERSION@">0.2</token>
|
||||
</macros>
|
||||
<requirements>
|
||||
<container type="docker">docker.io/anupkumar/docker-ml-jupyterlab:galaxy-integration-@VERSION@</container>
|
||||
@@ -19,71 +19,221 @@
|
||||
<environment_variable name="DISABLE_AUTH">true</environment_variable>
|
||||
<environment_variable name="API_KEY" inject="api_key" />
|
||||
</environment_variables>
|
||||
<command detect_errors="aggressive"><![CDATA[
|
||||
#import re
|
||||
<configfiles>
|
||||
<inputs name="inputs" filename="galaxy_inputs.json" data_style="staging_path_and_source_path"/>
|
||||
<configfile name="staging_script" filename="staging_script.py"><![CDATA[
|
||||
import json, os, shutil
|
||||
import os.path
|
||||
galaxy_inputs = "galaxy_inputs.json"
|
||||
galaxy_inputs_raw = "galaxy_inputs_raw.json"
|
||||
base_staging_path = "jupyter"
|
||||
base_data_staging_path = "galaxy_inputs"
|
||||
data_staging_path = os.path.join(base_staging_path, base_data_staging_path)
|
||||
os.makedirs(data_staging_path, exist_ok=True)
|
||||
shutil.copyfile(galaxy_inputs, os.path.join(data_staging_path, galaxy_inputs_raw))
|
||||
galaxy_input_dict = {}
|
||||
for user_input in json.load(open(galaxy_inputs, "r")).get("user_inputs", []):
|
||||
input_values = user_input['input_type']['input_value']
|
||||
input_name = user_input["input_name"]
|
||||
assert input_name not in galaxy_input_dict, ValueError("Each name can only be used once, but a duplicate was found: %s" % (input_name))
|
||||
if 'dataset' in user_input['input_type']['input_type_selector']:
|
||||
input_values2 = []
|
||||
if not isinstance(input_values, list):
|
||||
input_values = [input_values]
|
||||
for input_value_dict in input_values:
|
||||
path, filename = os.path.split(input_value_dict['staging_path'])
|
||||
input_value_dict2 = {"path": os.path.join(base_data_staging_path, input_name, path, filename), "metadata_files": []}
|
||||
path = os.path.join(data_staging_path, input_name, path)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
try:
|
||||
os.symlink(input_value_dict['source_path'], os.path.join(path, filename))
|
||||
except FileExistsError:
|
||||
pass
|
||||
for metadata_value_dict in input_value_dict['metadata_files']:
|
||||
path, filename = os.path.split(metadata_value_dict['staging_path'])
|
||||
input_value_dict2["metadata_files"].append(os.path.join(base_data_staging_path, input_name, path, filename))
|
||||
path = os.path.join(data_staging_path, input_name, path)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
try:
|
||||
os.symlink(metadata_value_dict['source_path'], os.path.join(path, filename))
|
||||
except FileExistsError:
|
||||
pass
|
||||
input_values2.append(input_value_dict2)
|
||||
input_values = input_values2
|
||||
galaxy_input_dict[input_name] = input_values
|
||||
with open(os.path.join(data_staging_path, galaxy_inputs), "w") as fh:
|
||||
json.dump(galaxy_input_dict, fh)
|
||||
]]>
|
||||
</configfile>
|
||||
<configfile name="galaxy_input_startup_script"><![CDATA[
|
||||
import json, os
|
||||
from galaxy_ie_helpers import get
|
||||
from galaxy_ie_helpers import put
|
||||
from galaxy_ie_helpers import get_galaxy_connection
|
||||
HISTORY_ID = os.environ.get('HISTORY_ID', None)
|
||||
try:
|
||||
GALAXY_INPUTS = json.load(open(os.path.join(os.environ.get("GALAXY_WORKING_DIR", ""), "jupyter", "galaxy_inputs", "galaxy_inputs.json")))
|
||||
except FileNotFoundError:
|
||||
GALAXY_INPUTS = {}
|
||||
]]>
|
||||
</configfile>
|
||||
</configfiles>
|
||||
|
||||
<command><![CDATA[
|
||||
python staging_script.py &&
|
||||
export GALAXY_WORKING_DIR=`pwd` &&
|
||||
mkdir -p ./jupyter/outputs/ &&
|
||||
mkdir -p ./jupyter/data &&
|
||||
mkdir -p ./jupyter/notebooks &&
|
||||
mkdir -p ./jupyter/elyra &&
|
||||
|
||||
#set $cleaned_name = re.sub('[^\w\-\.]', '_', str($input.element_identifier))
|
||||
ln -sf '$input' './jupyter/data/${cleaned_name}' &&
|
||||
|
||||
## change into the directory where the notebooks are located
|
||||
mkdir -p ./jupyter/outputs &&
|
||||
mkdir -p ./jupyter/galaxy_inputs &&
|
||||
cd ./jupyter/ &&
|
||||
export HOME=/home/\$NB_USER/ &&
|
||||
export PATH=/home/\$NB_USER/.local/bin:\$PATH &&
|
||||
cp '${galaxy_input_startup_script}' /home/\$NB_USER/.ipython/profile_default/startup/02-load.py &&
|
||||
#set $output_notebook_name = 'jupyterlab_notebook.ipynb'
|
||||
#if $mode.mode_select == 'scratch':
|
||||
## copy all notebooks, workflows and data
|
||||
cp /home/\$NB_USER/home_page.ipynb ./ &&
|
||||
cp /home/\$NB_USER/notebooks/*.ipynb ./notebooks/ &&
|
||||
cp /home/\$NB_USER/elyra/*.* ./elyra/ &&
|
||||
cp /home/\$NB_USER/data/*.tsv ./data/ &&
|
||||
## provide all rights to copied files
|
||||
chown -R \$NB_USER:users `pwd` &&
|
||||
chown \$NB_USER ./home_page.ipynb &&
|
||||
chown \$NB_USER ./notebooks/ &&
|
||||
chown \$NB_USER ./notebooks/*.ipynb &&
|
||||
chown \$NB_USER ./elyra/ &&
|
||||
chown \$NB_USER ./elyra/*.* &&
|
||||
jupyter trust ./home_page.ipynb &&
|
||||
jupyter trust ./notebooks/*.ipynb &&
|
||||
jupyter trust ./elyra/*.ipynb &&
|
||||
jupyter lab --allow-root --no-browser --NotebookApp.shutdown_button=True
|
||||
cp -r /home/\$NB_USER/data ./ &&
|
||||
cp -r /home/\$NB_USER/elyra ./ &&
|
||||
cp -r /home/\$NB_USER/notebooks ./ &&
|
||||
cp -r /home/\$NB_USER/usecases ./ &&
|
||||
cp -r /home/\$NB_USER/home_page.ipynb ./ &&
|
||||
jupyter lab --no-browser --NotebookApp.shutdown_button=True
|
||||
#else:
|
||||
#set $noteboook_name = re.sub('[^\w\-\.\s]', '_', str($mode.ipynb.element_identifier))
|
||||
cp '$mode.ipynb' './${noteboook_name}.ipynb' &&
|
||||
jupyter trust './${noteboook_name}.ipynb' &&
|
||||
#import re
|
||||
#set $cleaned_name = re.sub('[^\w\-\.]', '_', str($mode.ipynb.element_identifier))
|
||||
#if not $cleaned_name.endswith(".ipynb"'$jupyter_notebook'):
|
||||
#set $cleaned_name = "%s.ipynb" % $cleaned_name
|
||||
#end if
|
||||
cp '$mode.ipynb' '${cleaned_name}' &&
|
||||
jupyter trust '${cleaned_name}' &&
|
||||
|
||||
#if $mode.run_it:
|
||||
jupyter nbconvert --to notebook --execute --output ./ipython_galaxy_notebook.ipynb --allow-errors ./*.ipynb &&
|
||||
#set $noteboook_name = 'ipython_galaxy_notebook'
|
||||
jupyter nbconvert --to notebook --execute --output '${output_notebook_name}' --allow-errors '${cleaned_name}' &&
|
||||
cp '${output_notebook_name}' '$jupyter_notebook'
|
||||
#else:
|
||||
jupyter lab --allow-root --no-browser --NotebookApp.shutdown_button=True &&
|
||||
#set $output_notebook_name = $cleaned_name
|
||||
jupyter lab --no-browser --NotebookApp.shutdown_button=True
|
||||
#end if
|
||||
cp './${noteboook_name}.ipynb' '$jupyter_notebook'
|
||||
#end if
|
||||
]]>
|
||||
</command>
|
||||
<inputs>
|
||||
|
||||
<conditional name="mode">
|
||||
<param name="mode_select" type="select" label="Do you already have a notebook?" help="If not, no problem we will provide you with a default one.">
|
||||
<option value="scratch">Start with a fresh notebook</option>
|
||||
<option value="previous">Load a previous notebook</option>
|
||||
<param name="mode_select" type="select" label="Do you already have a notebook?" help="Select a set of default notebooks or load an existing one.">
|
||||
<option value="scratch">Start with default notebooks</option>
|
||||
<option value="previous">Load an existing notebook</option>
|
||||
</param>
|
||||
<when value="scratch"/>
|
||||
<when value="previous">
|
||||
<param name="ipynb" type="data" format="ipynb" label="IPython Notebook"/>
|
||||
<param name="run_it" type="boolean" truevalue="true" falsevalue="false" label="Execute notebook and return a new one."
|
||||
help="This option is useful in workflows when you just want to execute a notebook and not dive into the webfrontend."/>
|
||||
<param name="ipynb" type="data" format="ipynb" label="IPython Notebook" required="true"/>
|
||||
<param name="run_it" type="boolean" truevalue="true" falsevalue="false" label="Execute notebook and return a new one." help="This option is useful in workflows when you just want to execute a notebook and not dive into the webfrontend."/>
|
||||
</when>
|
||||
</conditional>
|
||||
<param name="input" type="data" optional="true" label="Include data into the environment"/>
|
||||
|
||||
<repeat name="user_inputs" title="User inputs">
|
||||
<param name="input_name" type="text" value="" label="Name for parameter" optional="False" help="Required. ASCII letters and numbers only.">
|
||||
<validator type="empty_field" message="Name is required"/>
|
||||
<sanitizer>
|
||||
<valid initial="string.ascii_letters,string.digits"/>
|
||||
</sanitizer>
|
||||
</param>
|
||||
<param name="description" type="text" label="Additional optional description" optional="true"/>
|
||||
<conditional name="input_type">
|
||||
<param name="input_type_selector" type="select" label="Choose the input type">
|
||||
<option value="dataset" selected="true">Dataset</option>
|
||||
<option value="dataset_multiple">Multiple datasets</option>
|
||||
<option value="dataset_collection">Dataset collection</option>
|
||||
<option value="dataset_collection_list">Dataset collection (list)</option>
|
||||
<option value="dataset_collection_paired">Dataset collection (paired)</option>
|
||||
<option value="dataset_collection_list_paired">Dataset collection (list:paired)</option>
|
||||
<option value="text">Text</option>
|
||||
<option value="integer">Integer</option>
|
||||
<option value="float">Floating point</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="color">Color selector</option>
|
||||
<option value="dataset_optional" selected="true">Optional Dataset</option>
|
||||
<option value="dataset_multiple_optional">Optional Multiple datasets</option>
|
||||
<option value="dataset_collection_optional">Optional Dataset collection</option>
|
||||
<option value="dataset_collection_list_optional">Optional Dataset collection (list)</option>
|
||||
<option value="dataset_collection_paired_optional">Optional Dataset collection (paired)</option>
|
||||
<option value="dataset_collection_list_paired_optional">Optional Dataset collection (list:paired)</option>
|
||||
<option value="text_optional">Optional Text</option>
|
||||
<option value="integer_optional">Optional Integer</option>
|
||||
<option value="float_optional">Optional Floating point</option>
|
||||
<option value="boolean_optional">Optional Boolean</option>
|
||||
<option value="color_optional">Optional Color selector</option>
|
||||
</param>
|
||||
<when value="dataset">
|
||||
<param name="input_value" type="data" format="data" label="Select value" multiple="false" optional="false"/>
|
||||
</when>
|
||||
<when value="dataset_multiple">
|
||||
<param name="input_value" type="data" format="data" label="Select value" multiple="true" optional="false"/>
|
||||
</when>
|
||||
<when value="dataset_collection">
|
||||
<param name="input_value" type="data_collection" format="data" label="Select value" optional="false"/>
|
||||
</when>
|
||||
<when value="dataset_collection_list">
|
||||
<param name="input_value" type="data_collection" format="data" label="Select value" collection_type="list" optional="false"/>
|
||||
</when>
|
||||
<when value="dataset_collection_paired">
|
||||
<param name="input_value" type="data_collection" format="data" label="Select value" collection_type="paired" optional="false"/>
|
||||
</when>
|
||||
<when value="dataset_collection_list_paired">
|
||||
<param name="input_value" type="data_collection" format="data" label="Select value" collection_type="list:paired" optional="false"/>
|
||||
</when>
|
||||
<when value="text">
|
||||
<param name="input_value" type="text" value="" label="Select value" optional="false"/>
|
||||
</when>
|
||||
<when value="integer">
|
||||
<param name="input_value" type="integer" value="" label="Select value" optional="false"/>
|
||||
</when>
|
||||
<when value="float">
|
||||
<param name="input_value" type="float" value="" label="Select value" optional="false"/>
|
||||
</when>
|
||||
<when value="boolean">
|
||||
<param name="input_value" type="boolean" truevalue="true" falsevalue="false" label="Select value" optional="false"/>
|
||||
</when>
|
||||
<when value="color">
|
||||
<param name="input_value" type="color" label="Select value" optional="false"/>
|
||||
</when>
|
||||
<when value="dataset_optional">
|
||||
<param name="input_value" type="data" format="data" label="Select value" multiple="false" optional="true"/>
|
||||
</when>
|
||||
<when value="dataset_multiple_optional">
|
||||
<param name="input_value" type="data" format="data" label="Select value" multiple="true" optional="true"/>
|
||||
</when>
|
||||
<when value="dataset_collection_optional">
|
||||
<param name="input_value" type="data_collection" format="data" label="Select value" optional="true"/>
|
||||
</when>
|
||||
<when value="dataset_collection_list_optional">
|
||||
<param name="input_value" type="data_collection" format="data" label="Select value" collection_type="list" optional="true"/>
|
||||
</when>
|
||||
<when value="dataset_collection_paired_optional">
|
||||
<param name="input_value" type="data_collection" format="data" label="Select value" collection_type="paired" optional="true"/>
|
||||
</when>
|
||||
<when value="dataset_collection_list_paired_optional">
|
||||
<param name="input_value" type="data_collection" format="data" label="Select value" collection_type="list:paired" optional="true"/>
|
||||
</when>
|
||||
<when value="text_optional">
|
||||
<param name="input_value" type="text" value="" label="Select value" optional="true"/>
|
||||
</when>
|
||||
<when value="integer_optional">
|
||||
<param name="input_value" type="integer" value="" label="Select value" optional="true"/>
|
||||
</when>
|
||||
<when value="float_optional">
|
||||
<param name="input_value" type="float" value="" label="Select value" optional="true"/>
|
||||
</when>
|
||||
<when value="boolean_optional">
|
||||
<param name="input_value" type="boolean" truevalue="true" falsevalue="false" label="Select value" optional="true"/>
|
||||
</when>
|
||||
<when value="color_optional">
|
||||
<param name="input_value" type="color" label="Select value" optional="true"/>
|
||||
</when>
|
||||
</conditional>
|
||||
</repeat>
|
||||
</inputs>
|
||||
<outputs>
|
||||
<data name="jupyter_notebook" format="ipynb" label="Executed Notebook" />
|
||||
<data name="jupyter_notebook" format="ipynb" label="Executed GPU JupyterLab Notebook"/>
|
||||
<collection name="output_collection" type="list" label="GPU JupyterLab notebook output collection">
|
||||
<discover_datasets pattern="__designation_and_ext__" directory="jupyter/outputs" visible="false"/>
|
||||
</collection>
|
||||
</outputs>
|
||||
<tests>
|
||||
<test expect_num_outputs="1">
|
||||
@@ -97,10 +247,9 @@
|
||||
JupyterLab is a next-generation web-based user interface for Project Jupyter. JupyterLab enables you to work with documents and activities such as Jupyter notebooks, text editors,
|
||||
terminals, and custom components in a flexible, integrated, and extensible manner.
|
||||
|
||||
Galaxy offers you to use Jupyter Lab directly in Galaxy accessing and interacting with Galaxy datasets as you like. A very common use-case is to
|
||||
Galaxy offers you to use JupyterLab directly in Galaxy accessing and interacting with Galaxy datasets as you like. A very common use-case is to
|
||||
do the heavy lifting such as performing computation on GPUs and data reduction steps in Galaxy and the plotting and more `interactive` part on smaller datasets in Jupyter Lab.
|
||||
|
||||
You can start with a new Jupyter Lab notebook from scratch and wait until the job starts running. Running job will provide you a link which can be opened in the same or another
|
||||
browser tab. This link opens Jupyter Lab notebook which can be used to prototype machine learning solutions.
|
||||
You can start with a new JupyterLab notebook from scratch and wait until the job starts running. Running job will provide you a link which can be opened in the same or another browser tab. This link opens JupyterLab notebook which can be used to prototype machine learning solutions.
|
||||
]]></help>
|
||||
</tool>
|
||||
|
||||
Reference in New Issue
Block a user