diff --git a/.github/workflows/integration_selenium.yaml b/.github/workflows/integration_selenium.yaml index 88f7e160dab..0ed645ac5fe 100644 --- a/.github/workflows/integration_selenium.yaml +++ b/.github/workflows/integration_selenium.yaml @@ -5,8 +5,6 @@ concurrency: cancel-in-progress: true env: GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' - GALAXY_TEST_SELENIUM_REMOTE: '1' - GALAXY_TEST_SELENIUM_REMOTE_PORT: "4444" GALAXY_SKIP_CLIENT_BUILD: '0' GALAXY_TEST_SELENIUM_RETRIES: 1 YARN_INSTALL_OPTS: --frozen-lockfile @@ -26,10 +24,6 @@ jobs: POSTGRES_DB: postgres ports: - 5432:5432 - selenium: - image: selenium/standalone-chrome:3.141.59 - ports: - - 4444:4444 steps: - name: Prune unused docker image, volumes and containers run: docker system prune -a -f @@ -48,6 +42,7 @@ jobs: - uses: mvdbeek/gha-yarn-cache@master with: yarn-lock-file: 'galaxy root/client/yarn.lock' + - uses: nanasess/setup-chromedriver@master - name: Run tests run: './run_tests.sh -integration test/integration_selenium' working-directory: 'galaxy root' diff --git a/client/README.md b/client/README.md index e091c142848..1295ec3a169 100644 --- a/client/README.md +++ b/client/README.md @@ -83,6 +83,10 @@ 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 + + CHANGE_ORIGIN=true GALAXY_URL="https://usegalaxy.org/" make client-dev-server + ## Changing Styles/CSS Galaxy uses Sass for its styling, which is a superset of CSS that compiles down diff --git a/client/package.json b/client/package.json index 69f82960618..ac34ee8d09a 100644 --- a/client/package.json +++ b/client/package.json @@ -29,7 +29,7 @@ "axios": "^0.21.1", "backbone": "1.4.0", "bootstrap": "4.5.0", - "bootstrap-vue": "^2.15.0", + "bootstrap-vue": "^2.21.2", "citation-js": "^0.5.1", "d3": "3", "date-fns": "^2.23.0", @@ -55,6 +55,7 @@ "markdown-it": "^11.0.0", "markdown-it-regexp": "^0.4.0", "moment": "2.29.1", + "object-hash": "^2.2.0", "popper.js": "^1.16.1", "pouchdb": "^7.2.2", "pouchdb-adapter-memory": "^7.2.2", @@ -62,6 +63,7 @@ "pouchdb-erase": "^1.0.2", "pouchdb-find": "^7.2.2", "pouchdb-upsert": "^2.2.0", + "pretty-bytes": "^5.6.0", "proper-skip-list": "^4.0.2", "pyre-to-regexp": "^0.0.5", "regression": "^2.0.1", @@ -88,7 +90,8 @@ "vueisotope": "^3.1.2", "vuex": "^3.4.0", "vuex-cache": "^3.2.0", - "vuex-persist": "^3.1.3" + "vuex-persist": "^3.1.3", + "vuex-persistedstate": "^4.0.0" }, "scripts": { "watch": "gulp && yarn run save-build-hash && yarn run webpack-watch", diff --git a/client/src/bundleEntries.js b/client/src/bundleEntries.js index d9b68cdd338..45bb0d8014e 100644 --- a/client/src/bundleEntries.js +++ b/client/src/bundleEntries.js @@ -32,6 +32,7 @@ export { default as LegacyGridView } from "legacy/grid/grid-view"; export { create_chart, create_histogram } from "reports/run_stats"; export { default as ToolshedGroups } from "toolshed/toolshed.groups"; export { default as IES } from "galaxy.interactive_environments"; +export { openGlobalUploadModal } from "components/Upload"; export { Toast } from "ui/toast"; // TODO: remove when external consumers are updated/gone (IES right now) diff --git a/client/src/components/Dataset/DatasetStorage/DatasetStorage.vue b/client/src/components/Dataset/DatasetStorage/DatasetStorage.vue index 7232d97e508..37f661fe0a1 100644 --- a/client/src/components/Dataset/DatasetStorage/DatasetStorage.vue +++ b/client/src/components/Dataset/DatasetStorage/DatasetStorage.vue @@ -66,7 +66,7 @@ export default { const description = storageInfo.description; this.storageInfo = storageInfo; if (description) { - this.descriptionRendered = MarkdownIt().render(storageInfo.description); + this.descriptionRendered = MarkdownIt({ html: true }).render(storageInfo.description); } else { this.descriptionRendered = null; } diff --git a/client/src/components/DatasetInformation/DatasetAttributes.test.js b/client/src/components/DatasetInformation/DatasetAttributes.test.js new file mode 100644 index 00000000000..524d2de08d3 --- /dev/null +++ b/client/src/components/DatasetInformation/DatasetAttributes.test.js @@ -0,0 +1,62 @@ +import Vue from "vue"; +import axios from "axios"; +import { mount } from "@vue/test-utils"; +import { getLocalVue } from "jest/helpers"; +import DatasetAttributes from "./DatasetAttributes"; +import MockProvider from "../providers/MockProvider"; +import MockAdapter from "axios-mock-adapter"; +import flushPromises from "flush-promises"; + +const localVue = getLocalVue(); + +function buildWrapper(conversion_disable = false) { + return mount(DatasetAttributes, { + propsData: { + datasetId: "dataset_id", + }, + localVue, + stubs: { + DatasetAttributesProvider: MockProvider({ + result: { + attribute_inputs: [{ name: "attribute_text", type: "text" }], + conversion_inputs: [{ name: "conversion_text", type: "text" }], + conversion_disable: conversion_disable, + datatype_inputs: [{ name: "datatype_text", type: "text" }], + permission_inputs: [{ name: "permission_text", type: "text" }], + }, + }), + FontAwesomeIcon: false, + FormElement: false, + }, + }); +} + +describe("DatasetAttributes", () => { + it("check rendering", async () => { + const axiosMock = new MockAdapter(axios); + axiosMock.onPut(`/dataset/set_edit`).reply(200, { message: "success", status: "success" }); + const wrapper = buildWrapper(); + await Vue.nextTick(); + expect(wrapper.findAll("button").length).toBe(6); + expect(wrapper.findAll("[tour_id=attribute_text]").length).toBe(1); + expect(wrapper.findAll("[tour_id=conversion_text]").length).toBe(1); + expect(wrapper.findAll("[tour_id=datatype_text]").length).toBe(1); + expect(wrapper.findAll("[tour_id=permission_text]").length).toBe(1); + expect(wrapper.findAll(".tab-pane").length).toBe(4); + const $button = wrapper.find("#dataset-attributes-default-save"); + await $button.trigger("click"); + await flushPromises(); + expect(wrapper.find("[role=alert]").text()).toBe("success"); + }); + + it("check rendering without conversion option", async () => { + const wrapper = buildWrapper(true); + await Vue.nextTick(); + expect(wrapper.findAll("button").length).toBe(5); + expect(wrapper.findAll("[tour_id=attribute_text]").length).toBe(1); + expect(wrapper.findAll("[tour_id=conversion_text]").length).toBe(0); + expect(wrapper.findAll("[tour_id=datatype_text]").length).toBe(1); + expect(wrapper.findAll("[tour_id=permission_text]").length).toBe(1); + expect(wrapper.findAll(".tab-pane").length).toBe(3); + }); +}); diff --git a/client/src/components/DatasetInformation/DatasetAttributes.vue b/client/src/components/DatasetInformation/DatasetAttributes.vue new file mode 100644 index 00000000000..4685833941a --- /dev/null +++ b/client/src/components/DatasetInformation/DatasetAttributes.vue @@ -0,0 +1,134 @@ + + + diff --git a/client/src/components/DatasetInformation/DatasetError.test.js b/client/src/components/DatasetInformation/DatasetError.test.js new file mode 100644 index 00000000000..9fd95ad9310 --- /dev/null +++ b/client/src/components/DatasetInformation/DatasetError.test.js @@ -0,0 +1,64 @@ +import { mount } from "@vue/test-utils"; +import { getLocalVue } from "jest/helpers"; +import DatasetError from "./DatasetError"; +import MockProvider from "../providers/MockProvider"; + +jest.mock("components/WorkflowInvocationState/providers", () => { + return {}; // stubbed below +}); + +const localVue = getLocalVue(); + +function buildWrapper(has_duplicate_inputs = true, has_empty_inputs = true, user_email = "") { + return mount(DatasetError, { + propsData: { + datasetId: "dataset_id", + }, + localVue, + stubs: { + JobDetailsProvider: MockProvider({ + result: { + tool_id: "tool_id", + tool_stderr: "tool_stderr", + job_stderr: "job_stderr", + job_messages: [{ desc: "message_1" }, { desc: "message_2" }], + user_email: user_email, + }, + }), + JobProblemProvider: MockProvider({ + result: { has_duplicate_inputs: has_duplicate_inputs, has_empty_inputs: has_empty_inputs }, + }), + DatasetProvider: MockProvider({ + resultLabel: "item", + result: { id: "dataset_id", creating_job: "creating_job" }, + }), + FontAwesomeIcon: false, + FormElement: false, + }, + }); +} + +describe("DatasetError", () => { + it("check props with common problems", async () => { + const wrapper = buildWrapper(); + expect(wrapper.find("#dataset-error-tool-id").text()).toBe("tool_id"); + expect(wrapper.find("#dataset-error-tool-stderr").text()).toBe("tool_stderr"); + expect(wrapper.find("#dataset-error-job-stderr").text()).toBe("job_stderr"); + const messages = wrapper.findAll("#dataset-error-job-messages .code"); + expect(messages.at(0).text()).toBe("message_1"); + expect(messages.at(1).text()).toBe("message_2"); + expect(wrapper.find("#dataset-error-has-empty-inputs")).toBeDefined(); + expect(wrapper.find("#dataset-error-has-duplicate-inputs")).toBeDefined(); + expect(wrapper.findAll("#dataset-error-email").length).toBe(1); + }); + + it("check props without common problems", async () => { + const wrapper = buildWrapper(false, false, "user_email"); + expect(wrapper.find("#dataset-error-tool-id").text()).toBe("tool_id"); + expect(wrapper.find("#dataset-error-tool-stderr").text()).toBe("tool_stderr"); + expect(wrapper.find("#dataset-error-job-stderr").text()).toBe("job_stderr"); + expect(wrapper.findAll("#dataset-error-has-empty-inputs").length).toBe(0); + expect(wrapper.findAll("#dataset-error-has-duplicate-inputs").length).toBe(0); + expect(wrapper.findAll("#dataset-error-email").length).toBe(0); + }); +}); diff --git a/client/src/components/DatasetInformation/DatasetError.vue b/client/src/components/DatasetInformation/DatasetError.vue new file mode 100644 index 00000000000..5618117ab4d --- /dev/null +++ b/client/src/components/DatasetInformation/DatasetError.vue @@ -0,0 +1,150 @@ + + + diff --git a/client/src/components/DatasetInformation/services.js b/client/src/components/DatasetInformation/services.js new file mode 100644 index 00000000000..310c5b51ca0 --- /dev/null +++ b/client/src/components/DatasetInformation/services.js @@ -0,0 +1,33 @@ +import axios from "axios"; +import { rethrowSimple } from "utils/simple-error"; +import { getAppRoot } from "onload/loadConfig"; + +export async function sendErrorReport(dataset, message, email) { + const payload = { + dataset_id: dataset.id, + message, + email, + }; + const url = `${getAppRoot()}api/jobs/${dataset.creating_job}/error`; + try { + const { data } = await axios.post(url, payload); + return data.messages; + } catch (e) { + rethrowSimple(e); + } +} + +export async function setAttributes(datasetId, settings, operation) { + const payload = { + dataset_id: datasetId, + operation: operation, + ...settings, + }; + const url = `${getAppRoot()}dataset/set_edit`; + try { + const { data } = await axios.put(url, payload); + return data; + } catch (e) { + rethrowSimple(e); + } +} diff --git a/client/src/components/Form/Elements/FormInput.vue b/client/src/components/Form/Elements/FormInput.vue index 05758a19b28..d2cc0264769 100644 --- a/client/src/components/Form/Elements/FormInput.vue +++ b/client/src/components/Form/Elements/FormInput.vue @@ -1,6 +1,6 @@