diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..6ae457a2aeb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Galaxy Version and/or server at which you observed the bug** +Galaxy Version: (check /api/version if you don't know) +Commit: (run `git rev-parse HEAD` if you run this Galaxy server) + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from pull_request_template.md rename to .github/pull_request_template.md 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/.github/workflows/selenium_beta.yaml b/.github/workflows/selenium_beta.yaml new file mode 100644 index 00000000000..a0a90bee180 --- /dev/null +++ b/.github/workflows/selenium_beta.yaml @@ -0,0 +1,51 @@ +name: Selenium tests (beta history panel) +on: [push, pull_request] +env: + GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8' + GALAXY_TEST_SKIP_FLAKEY_TESTS_ON_ERROR: 1 + GALAXY_TEST_SELENIUM_RETRIES: 1 + GALAXY_TEST_SELENIUM_BETA_HISTORY: 1 +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.7'] + chunk: [0, 1, 2] + continue-on-error: true + services: + postgres: + image: postgres:13 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + steps: + - uses: actions/checkout@v2 + with: + path: 'galaxy root' + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Cache pip dir + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('galaxy root/requirements.txt') }} + - uses: nanasess/setup-chromedriver@master + - name: Run tests + run: ./run_tests.sh -selenium lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }} || true + working-directory: 'galaxy root' + - uses: actions/upload-artifact@v2 + with: + name: Selenium beta history panel test results (${{ matrix.python-version }}, ${{ matrix.chunk }}) + path: 'galaxy root/database/test_errors' + - uses: actions/upload-artifact@v2 + with: + name: Selenium beta history panel test results (${{ matrix.python-version }}) + path: 'galaxy root/run_selenium_tests.html' + \ No newline at end of file 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 8f5b4ad7b4b..053fc60a063 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.0-alpha.5", "d3": "3", "date-fns": "^2.21.1", @@ -55,6 +55,7 @@ "markdown-it": "^11.0.0", "markdown-it-regexp": "^0.4.0", "moment": "2.26.0", + "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", @@ -85,7 +87,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 a4bfdcd9dcd..824504c7b43 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/Common/ButtonSpinner.vue b/client/src/components/Common/ButtonSpinner.vue new file mode 100644 index 00000000000..56531098ef6 --- /dev/null +++ b/client/src/components/Common/ButtonSpinner.vue @@ -0,0 +1,36 @@ + + diff --git a/client/src/components/Common/Webhook.vue b/client/src/components/Common/Webhook.vue new file mode 100644 index 00000000000..a3885849690 --- /dev/null +++ b/client/src/components/Common/Webhook.vue @@ -0,0 +1,27 @@ + + 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..a35b56a565f --- /dev/null +++ b/client/src/components/DatasetInformation/DatasetError.vue @@ -0,0 +1,140 @@ + + + diff --git a/client/src/components/DatasetInformation/DatasetErrorDetails.vue b/client/src/components/DatasetInformation/DatasetErrorDetails.vue new file mode 100644 index 00000000000..e2f98519d44 --- /dev/null +++ b/client/src/components/DatasetInformation/DatasetErrorDetails.vue @@ -0,0 +1,46 @@ + + + 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/FormBoolean.test.js b/client/src/components/Form/Elements/FormBoolean.test.js new file mode 100644 index 00000000000..90149c2442c --- /dev/null +++ b/client/src/components/Form/Elements/FormBoolean.test.js @@ -0,0 +1,32 @@ +import { mount } from "@vue/test-utils"; +import { getLocalVue } from "jest/helpers"; +import FormBoolean from "./FormBoolean"; +import Vue from "vue"; + +const localVue = getLocalVue(); + +describe("FormBoolean", () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(FormBoolean, { + propsData: { + value: false, + }, + localVue, + }); + }); + + it("check initial value and value change", async () => { + const input = wrapper.find("input"); + expect(wrapper.vm.currentValue).toBe(false); + wrapper.setProps({ value: true }); + await Vue.nextTick(); + expect(wrapper.vm.currentValue).toBe(true); + input.trigger("click"); + expect(input.element.checked).toBe(false); + input.trigger("click"); + expect(input.element.checked).toBe(true); + expect(wrapper.emitted().input[0][0]).toBe(true); + }); +}); diff --git a/client/src/components/Form/Elements/FormBoolean.vue b/client/src/components/Form/Elements/FormBoolean.vue new file mode 100644 index 00000000000..b926c2c3738 --- /dev/null +++ b/client/src/components/Form/Elements/FormBoolean.vue @@ -0,0 +1,24 @@ + + + diff --git a/client/src/components/Form/FormInput.test.js b/client/src/components/Form/Elements/FormInput.test.js similarity index 88% rename from client/src/components/Form/FormInput.test.js rename to client/src/components/Form/Elements/FormInput.test.js index ebe24aa5960..c9b7eb9247b 100644 --- a/client/src/components/Form/FormInput.test.js +++ b/client/src/components/Form/Elements/FormInput.test.js @@ -24,7 +24,7 @@ describe("FormInput", () => { expect(input.element.value).toBe("initial_value"); await input.setValue("new_value"); expect(input.element.value).toBe("new_value"); - expect(wrapper.emitted().onChange[0][0]).toBe("new_value"); + expect(wrapper.emitted().input[0][0]).toBe("new_value"); }); it("check switching to text area", async () => { @@ -33,6 +33,6 @@ describe("FormInput", () => { expect(input.element.value).toBe("initial_value"); await input.setValue("new_value"); expect(input.element.value).toBe("new_value"); - expect(wrapper.emitted().onChange[0][0]).toBe("new_value"); + expect(wrapper.emitted().input[0][0]).toBe("new_value"); }); }); diff --git a/client/src/components/Form/Elements/FormInput.vue b/client/src/components/Form/Elements/FormInput.vue new file mode 100644 index 00000000000..d2cc0264769 --- /dev/null +++ b/client/src/components/Form/Elements/FormInput.vue @@ -0,0 +1,32 @@ +