`__.
diff --git a/client/galaxy/scripts/bundleEntries.js b/client/galaxy/scripts/bundleEntries.js
index 2818689602f..8e6efed234d 100644
--- a/client/galaxy/scripts/bundleEntries.js
+++ b/client/galaxy/scripts/bundleEntries.js
@@ -95,6 +95,7 @@ export { mountJobMetrics } from "components/JobMetrics";
export { mountJobParameters } from "components/JobParameters";
export { mountWorkflowEditor } from "components/Workflow/Editor/mount";
export { mountPageDisplay } from "components/PageDisplay";
+export { mountDestinationParams } from "components/JobDestinationParams";
// Used in common.mako
export { default as store } from "storemodern";
diff --git a/client/galaxy/scripts/components/JobDestinationParams/JobDestinationParams.test.js b/client/galaxy/scripts/components/JobDestinationParams/JobDestinationParams.test.js
new file mode 100644
index 00000000000..1a0eb75321a
--- /dev/null
+++ b/client/galaxy/scripts/components/JobDestinationParams/JobDestinationParams.test.js
@@ -0,0 +1,67 @@
+import Vuex from "vuex";
+import axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import { mount, createLocalVue } from "@vue/test-utils";
+import { createStore } from "../../store";
+import flushPromises from "flush-promises";
+import JobDestinationParams from "./JobDestinationParams";
+import jobDestinationResponse from "./testData/jobDestinationResponse";
+
+const JOB_ID = "foo_job_id";
+
+describe("JobDestinationParams/JobDestinationParams.vue", () => {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const responseKeys = Object.keys(jobDestinationResponse);
+
+ let testStore, axiosMock, wrapper;
+
+ beforeEach(async () => {
+ axiosMock = new MockAdapter(axios);
+ testStore = createStore();
+ const propsData = {
+ jobId: JOB_ID,
+ };
+ axiosMock.onGet(`/api/jobs/${JOB_ID}/destination_params`).reply(200, jobDestinationResponse);
+ wrapper = mount(JobDestinationParams, {
+ store: testStore,
+ propsData,
+ localVue,
+ });
+ await flushPromises();
+ assert(responseKeys.length > 0, "test data is invalid!");
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
+ it("destination parameters should exist", async () => {
+ expect(Object.keys(wrapper.vm.jobDestinationParams).length).to.equals(responseKeys.length);
+ expect(wrapper.vm.jobId).to.equals(JOB_ID);
+ expect(wrapper.vm.jobDestinationParams["docker_net"]).to.equals("bridge");
+ expect(wrapper.vm.jobDestinationParams["docker_set_user"]).to.equals(null);
+ });
+
+ it("destination parameters should be rendered", async () => {
+ console.log(wrapper.html());
+ const paramsTable = wrapper.find("#destination_parameters");
+ expect(paramsTable.isVisible()).to.equals(true);
+ const params = paramsTable.findAll("tbody > tr");
+ expect(params.length).to.equals(responseKeys.length);
+
+ for (let counter = 0; counter < responseKeys.length - 1; counter++) {
+ const parameter = params.at(counter).findAll("td");
+ const parameterTitle = parameter.at(0).text();
+ const parameterValue = parameter.at(1).text();
+
+ assert(responseKeys.includes(parameterTitle), "rendered parameter should exist in test data!");
+ // since we render null as an empty string, rendered empty string should always equal null in test data
+ assert(
+ jobDestinationResponse[parameterTitle] === (parameterValue === "" ? null : parameterValue),
+ "parameter value is not equal to test data!"
+ );
+ }
+ });
+});
diff --git a/client/galaxy/scripts/components/JobDestinationParams/JobDestinationParams.vue b/client/galaxy/scripts/components/JobDestinationParams/JobDestinationParams.vue
new file mode 100644
index 00000000000..7732edf8b90
--- /dev/null
+++ b/client/galaxy/scripts/components/JobDestinationParams/JobDestinationParams.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+ {{ title }}
+ {{ value }}
+
+
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/JobDestinationParams/index.js b/client/galaxy/scripts/components/JobDestinationParams/index.js
new file mode 100644
index 00000000000..d7e3a304577
--- /dev/null
+++ b/client/galaxy/scripts/components/JobDestinationParams/index.js
@@ -0,0 +1,3 @@
+export { default as JobDestinationParams } from "./JobDestinationParams";
+
+export { mountDestinationParams } from "./mount";
diff --git a/client/galaxy/scripts/components/JobDestinationParams/mount.js b/client/galaxy/scripts/components/JobDestinationParams/mount.js
new file mode 100644
index 00000000000..271db1a40b9
--- /dev/null
+++ b/client/galaxy/scripts/components/JobDestinationParams/mount.js
@@ -0,0 +1,13 @@
+/**
+ * Endpoint for mounting job metrics from non-Vue environment.
+ */
+import $ from "jquery";
+import JobDestinationParams from "./JobDestinationParams.vue";
+import { mountVueComponent } from "utils/mountVueComponent";
+
+export const mountDestinationParams = (propsData = {}) => {
+ $(".job-destination-parameters").each((index, el) => {
+ propsData.jobId = $(el).attr("job_id");
+ mountVueComponent(JobDestinationParams)(propsData, el);
+ });
+};
diff --git a/client/galaxy/scripts/components/JobDestinationParams/testData/jobDestinationResponse.json b/client/galaxy/scripts/components/JobDestinationParams/testData/jobDestinationResponse.json
new file mode 100644
index 00000000000..42070b6f77e
--- /dev/null
+++ b/client/galaxy/scripts/components/JobDestinationParams/testData/jobDestinationResponse.json
@@ -0,0 +1,12 @@
+{
+ "Runner": "local",
+ "Runner Job ID": "23027",
+ "Handler": "main.web.1",
+ "docker_auto_rm": "true",
+ "docker_enabled": "true",
+ "docker_net": "bridge",
+ "docker_set_user": null,
+ "docker_sudo": "false",
+ "docker_volumes": "$galaxy_root:ro,$tool_directory:ro,$job_directory:rw,$working_directory:rw,$default_file_path:ro",
+ "require_container": "true"
+}
diff --git a/client/galaxy/scripts/components/JobMetrics/JobMetrics.test.js b/client/galaxy/scripts/components/JobMetrics/JobMetrics.test.js
index 0a97c76206e..ccf5122e802 100644
--- a/client/galaxy/scripts/components/JobMetrics/JobMetrics.test.js
+++ b/client/galaxy/scripts/components/JobMetrics/JobMetrics.test.js
@@ -5,6 +5,7 @@ import { mount, createLocalVue } from "@vue/test-utils";
import { createStore } from "../../store";
import flushPromises from "flush-promises";
import JobMetrics from "./JobMetrics";
+import ec2 from "./ec2.json";
const JOB_ID = "moo";
@@ -65,4 +66,63 @@ describe("JobMetrics/JobMetrics.vue", () => {
expect(metricsTables.at(1).find(".metrics_plugin_title").text()).to.equals("extended");
expect(metricsTables.at(1).findAll("tr").length).to.equals(1);
});
+
+ it("should render correct AWS Estimates", async () => {
+ let deriveRenderedAwsEstimate = async (cores, seconds, memory) => {
+ const JOB_ID = Math.random().toString(36).substring(2);
+ const propsData = {
+ jobId: JOB_ID,
+ aws_estimate: "True",
+ };
+ const metricsResponse = [
+ { plugin: "core", name: "galaxy_slots", raw_value: cores },
+ { plugin: "core", name: "runtime_seconds", raw_value: seconds },
+ { plugin: "core", name: "galaxy_memory_mb", raw_value: memory },
+ ];
+ axiosMock.onGet(`/api/jobs/${JOB_ID}/metrics`).reply(200, metricsResponse);
+ const wrapper = mount(JobMetrics, {
+ store: testStore,
+ propsData,
+ localVue,
+ });
+ // Wait for axios and rendering.
+ await flushPromises();
+
+ const estimates = {};
+
+ if (!wrapper.find("#aws-estimate").exists()) return false;
+
+ estimates.cost = wrapper.find("#aws-estimate > b").text();
+ estimates.vcpus = wrapper.find("#aws_vcpus").text();
+ estimates.cpu = wrapper.find("#aws_cpu").text();
+ estimates.mem = wrapper.find("#aws_mem").text();
+ estimates.name = wrapper.find("#aws_name").text();
+
+ return estimates;
+ };
+ let assertAwsInstance = (estimates) => {
+ const instance = ec2.find((instance) => estimates.name === instance.name);
+ expect(estimates.mem).to.equals(instance.mem.toString());
+ expect(estimates.vcpus).to.equals(instance.vcpus.toString());
+ expect(estimates.cpu).to.equals(instance.cpu.toString());
+ };
+
+ let estimates_small = await deriveRenderedAwsEstimate("1.0000000", "9.0000000", "2048.0000000");
+
+ expect(estimates_small.name).to.equals("t2.small");
+ expect(estimates_small.cost).to.equals("0.00 USD");
+ assertAwsInstance(estimates_small);
+
+ let estimates_large = await deriveRenderedAwsEstimate("40.0000000", "18000.0000000", "194560.0000000");
+ expect(estimates_large.name).to.equals("m5d.12xlarge");
+ expect(estimates_large.cost).to.equals("16.32 USD");
+ assertAwsInstance(estimates_large);
+
+ let estimates_not_available = await deriveRenderedAwsEstimate(
+ "99999.0000000",
+ "18000.0000000",
+ "99999.0000000"
+ );
+ expect(estimates_not_available).to.equals(false);
+ });
});
diff --git a/client/galaxy/scripts/components/JobMetrics/JobMetrics.vue b/client/galaxy/scripts/components/JobMetrics/JobMetrics.vue
index f0bcd270258..9e148322c30 100644
--- a/client/galaxy/scripts/components/JobMetrics/JobMetrics.vue
+++ b/client/galaxy/scripts/components/JobMetrics/JobMetrics.vue
@@ -12,12 +12,27 @@
+
+
+
AWS estimate
+ {{ awsEstimate.price }} USD
+ This job requested {{ awsEstimate.vcpus }} cores and {{ awsEstimate.memory }} Gb. Given this, the smallest
+ EC2 machine we could find is {{ awsEstimate.instance.name }} ({{ awsEstimate.instance.mem }}
+ GB / {{ awsEstimate.instance.vcpus }} vCPUs /
+ {{ awsEstimate.instance.cpu }} ). That instance is priced at {{ awsEstimate.instance.price }} USD/hour.
+ Please note, that those numbers are only estimates, all jobs are always free of charge for all users.
+
diff --git a/client/galaxy/scripts/components/RuleBuilder/IdentifierDisplay.vue b/client/galaxy/scripts/components/RuleBuilder/IdentifierDisplay.vue
new file mode 100644
index 00000000000..54f1ce1ca4b
--- /dev/null
+++ b/client/galaxy/scripts/components/RuleBuilder/IdentifierDisplay.vue
@@ -0,0 +1,54 @@
+
+
+ Set {{ columnsLabel }} as {{ typeDisplay }}
+
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/RuleBuilder/RegularExpressionInput.vue b/client/galaxy/scripts/components/RuleBuilder/RegularExpressionInput.vue
new file mode 100644
index 00000000000..36f65600040
--- /dev/null
+++ b/client/galaxy/scripts/components/RuleBuilder/RegularExpressionInput.vue
@@ -0,0 +1,43 @@
+
+
+ {{ label }}
+
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/RuleBuilder/RuleComponent.vue b/client/galaxy/scripts/components/RuleBuilder/RuleComponent.vue
new file mode 100644
index 00000000000..474e6cbc4a4
--- /dev/null
+++ b/client/galaxy/scripts/components/RuleBuilder/RuleComponent.vue
@@ -0,0 +1,48 @@
+
+
+
+
+ {{ cancelLabel }}
+ {{ applyLabel }}
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/RuleBuilder/RuleDisplay.vue b/client/galaxy/scripts/components/RuleBuilder/RuleDisplay.vue
new file mode 100644
index 00000000000..1d52daf8011
--- /dev/null
+++ b/client/galaxy/scripts/components/RuleBuilder/RuleDisplay.vue
@@ -0,0 +1,54 @@
+
+
+
+ {{ title }}
+
+
+
+
+ {{ rule.warn }}
+
+
+ {{ rule.error }}
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/RuleBuilder/RuleModalFooter.vue b/client/galaxy/scripts/components/RuleBuilder/RuleModalFooter.vue
new file mode 100644
index 00000000000..fc2a3bfe739
--- /dev/null
+++ b/client/galaxy/scripts/components/RuleBuilder/RuleModalFooter.vue
@@ -0,0 +1,10 @@
+
+
+
diff --git a/client/galaxy/scripts/components/RuleBuilder/RuleModalHeader.vue b/client/galaxy/scripts/components/RuleBuilder/RuleModalHeader.vue
new file mode 100644
index 00000000000..9ea12aefa61
--- /dev/null
+++ b/client/galaxy/scripts/components/RuleBuilder/RuleModalHeader.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/client/galaxy/scripts/components/RuleBuilder/RuleModalMiddle.vue b/client/galaxy/scripts/components/RuleBuilder/RuleModalMiddle.vue
new file mode 100644
index 00000000000..35fca19d00a
--- /dev/null
+++ b/client/galaxy/scripts/components/RuleBuilder/RuleModalMiddle.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/client/galaxy/scripts/components/RuleBuilder/RuleTargetComponent.vue b/client/galaxy/scripts/components/RuleBuilder/RuleTargetComponent.vue
new file mode 100644
index 00000000000..aeb4d07d1df
--- /dev/null
+++ b/client/galaxy/scripts/components/RuleBuilder/RuleTargetComponent.vue
@@ -0,0 +1,34 @@
+
+ {{ title }}
+
+
+
diff --git a/client/galaxy/scripts/components/RuleBuilder/StateDiv.vue b/client/galaxy/scripts/components/RuleBuilder/StateDiv.vue
new file mode 100644
index 00000000000..2f06d8daf7f
--- /dev/null
+++ b/client/galaxy/scripts/components/RuleBuilder/StateDiv.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/RuleCollectionBuilder.vue b/client/galaxy/scripts/components/RuleCollectionBuilder.vue
index d4d99436d37..185ae53f7eb 100644
--- a/client/galaxy/scripts/components/RuleCollectionBuilder.vue
+++ b/client/galaxy/scripts/components/RuleCollectionBuilder.vue
@@ -576,360 +576,22 @@ import RuleDefs from "mvc/rules/rule-definitions";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import Select2 from "components/Select2";
+import ColumnSelector from "components/RuleBuilder/ColumnSelector";
+import RegularExpressionInput from "components/RuleBuilder/RegularExpressionInput";
+import RuleDisplay from "components/RuleBuilder/RuleDisplay";
+import IdentifierDisplay from "components/RuleBuilder/IdentifierDisplay";
+import RuleTargetComponent from "components/RuleBuilder/RuleTargetComponent";
+import RuleComponent from "components/RuleBuilder/RuleComponent";
+import RuleModalHeader from "components/RuleBuilder/RuleModalHeader";
+import RuleModalMiddle from "components/RuleBuilder/RuleModalMiddle";
+import RuleModalFooter from "components/RuleBuilder/RuleModalFooter";
+import StateDiv from "components/RuleBuilder/StateDiv";
Vue.use(BootstrapVue);
const RULES = RuleDefs.RULES;
const MAPPING_TARGETS = RuleDefs.MAPPING_TARGETS;
-const ColumnSelector = {
- template: `
-
-
- {{ label }}
-
- {{ col }}
-
-
-
-
-
-
{{ label }}
-
-
-
- {{ colHeaders[targetEl] }}
-
-
-
-
-
-
- ... {{ l("Assign Another Column") }}
-
-
-
-
- {{ col }}
-
-
-
-
-
- `,
- data: function () {
- return {
- l: _l,
- };
- },
- props: {
- target: {
- required: true,
- },
- label: {
- required: false,
- type: String,
- default: _l("From Column"),
- },
- help: {
- required: false,
- },
- colHeaders: {
- type: Array,
- required: true,
- },
- multiple: {
- type: Boolean,
- required: false,
- default: false,
- },
- ordered: {
- type: Boolean,
- required: false,
- default: false,
- },
- valueAsList: {
- type: Boolean,
- required: false,
- default: false,
- },
- orderedEdit: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- remainingHeaders() {
- const colHeaders = this.colHeaders;
- if (!this.multiple) {
- return colHeaders;
- }
- const remaining = {};
- for (const key in colHeaders) {
- if (this.target.indexOf(parseInt(key)) === -1) {
- remaining[key] = colHeaders[key];
- }
- }
- return remaining;
- },
- title() {
- return _l("Select a column");
- },
- },
- methods: {
- handleInput(value) {
- if (this.multiple) {
- // https://stackoverflow.com/questions/262427/why-does-parseint-yield-nan-with-arraymap
- const val = value.map((idx) => parseInt(idx));
- this.$emit("update:target", val);
- } else {
- let val = parseInt(value);
- if (this.valueAsList) {
- val = [val];
- }
- this.$emit("update:target", val);
- }
- },
- handleAdd(value) {
- this.target.push(parseInt(value));
- this.$emit("update:orderedEdit", false);
- },
- handleRemove(index) {
- this.target.splice(index, 1);
- },
- moveUp(value) {
- const swapVal = this.target[value - 1];
- Vue.set(this.target, value - 1, this.target[value]);
- Vue.set(this.target, value, swapVal);
- },
- },
- components: {
- Select2,
- },
-};
-
-const RegularExpressionInput = {
- template: `
-
- {{ label }}
-
-
-
- `,
- props: {
- target: {
- required: true,
- },
- },
- computed: {
- label() {
- return _l("Regular Expression");
- },
- title() {
- return _l("Enter a regular expression.");
- },
- popoverTitle() {
- return _l("Regular Expressions");
- },
- popoverContent() {
- return _l(
- `Regular expressions are patterns used to match character combinations in strings. This input accepts Python-style regular expressions, find more information about these in this Python for Biologists tutorial .`
- );
- },
- },
-};
-
-const RuleDisplay = {
- template: `
-
-
- {{ title }}
-
-
-
-
- {{ rule.warn }}
-
-
- {{ rule.error }}
-
-
- `,
- props: {
- rule: {
- required: true,
- type: Object,
- },
- colHeaders: {
- type: Array,
- required: true,
- },
- },
- computed: {
- title() {
- const ruleType = this.rule.type;
- return RULES[ruleType].display(this.rule, this.colHeaders);
- },
- editTitle() {
- return _l("Edit this rule.");
- },
- removeTitle() {
- return _l("Remove this rule.");
- },
- },
- methods: {
- edit() {
- this.$emit("edit");
- },
- remove() {
- this.$emit("remove");
- },
- },
-};
-
-const IdentifierDisplay = {
- template: `
-
- Set {{ columnsLabel }} as {{ typeDisplay }}
-
-
-
- `,
- props: {
- type: {
- type: String,
- required: true,
- },
- columns: {
- required: true,
- },
- colHeaders: {
- type: Array,
- required: true,
- },
- },
- methods: {
- remove() {
- this.$emit("remove");
- },
- edit() {
- this.$emit("edit");
- },
- },
- computed: {
- typeDisplay() {
- return MAPPING_TARGETS[this.type].label;
- },
- help() {
- return MAPPING_TARGETS[this.type].help || "";
- },
- titleEdit() {
- return _l("Edit column definition");
- },
- titleRemove() {
- return _l("Remove this column definition");
- },
- columnsLabel() {
- return RuleDefs.columnDisplay(this.columns, this.colHeaders);
- },
- },
-};
-
-const RuleTargetComponent = {
- template: `{{title}} `,
- props: {
- ruleType: {
- type: String,
- required: true,
- },
- builder: {
- required: true,
- },
- },
- computed: {
- linkClassName() {
- return "rule-link-" + this.ruleType.replace(/_/g, "-");
- },
- title() {
- return RULES[this.ruleType].title;
- },
- },
-};
-
-const RuleComponent = {
- template: `
-
-
-
- {{ cancelLabel }}
- {{ applyLabel }}
-
-
`,
- data: function () {
- return {
- applyLabel: _l("Apply"),
- cancelLabel: _l("Cancel"),
- };
- },
- props: {
- ruleType: {
- type: String,
- required: true,
- },
- displayRuleType: {
- required: true,
- },
- builder: {
- required: true,
- },
- },
- methods: {
- cancel() {
- this.builder.displayRuleType = null;
- },
- okay() {
- this.builder.handleRuleSave(this.ruleType);
- this.cancel();
- },
- },
- computed: {
- typeToClass() {
- return "rule-edit-" + this.ruleType.replace(/_/g, "-");
- },
- },
-};
-
-const StateDiv = {
- template: `
-
-
-
`,
-};
-
-const RuleModalHeader = {
- template: ``,
-};
-
-const RuleModalMiddle = {
- template: `
`,
-};
-
-const RuleModalFooter = {
- template: `
- `,
-};
-
export default {
data: function () {
let orientation = "vertical";
@@ -1977,19 +1639,19 @@ export default {