Merge remote-tracking branch 'upstream/release_23.0' into dev

This commit is contained in:
Dannon Baker
2023-01-27 13:23:11 -05:00
28 changed files with 519 additions and 239 deletions
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -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>
+1 -9
View File
@@ -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>
+2 -7
View File
@@ -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);
}
+3 -1
View File
@@ -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;
@@ -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;
}
+1 -1
View File
@@ -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({});
+6 -5
View File
@@ -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,
+1 -1
View File
@@ -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();
+17 -1
View File
@@ -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);
+6 -2
View File
@@ -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",
+2 -2
View File
@@ -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
+2 -4
View File
@@ -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>