mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #16020 from martenson/jobs-filtering
implement admin jobs filtering
This commit is contained in:
@@ -4,14 +4,39 @@ export default {
|
||||
components: {
|
||||
IndexFilter,
|
||||
},
|
||||
props: {
|
||||
inputDebounceDelay: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filter: "",
|
||||
implicitFilter: null,
|
||||
helpHtml: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isFiltered() {
|
||||
return !!this.filter;
|
||||
return Boolean(this.filter);
|
||||
},
|
||||
effectiveFilter() {
|
||||
let filter = this.filter;
|
||||
const implicitFilter = this.implicitFilter;
|
||||
if (implicitFilter && filter) {
|
||||
filter = `${implicitFilter} ${filter}`;
|
||||
} else if (implicitFilter) {
|
||||
filter = implicitFilter;
|
||||
}
|
||||
return filter;
|
||||
},
|
||||
filterAttrs() {
|
||||
return {
|
||||
"debounce-delay": this.inputDebounceDelay,
|
||||
placeholder: this.titleSearch,
|
||||
"help-html": this.helpHtml,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -43,4 +43,16 @@ describe("filtersMixin.js", () => {
|
||||
wrapper.vm.appendTagFilter("name", "foobar");
|
||||
expect(wrapper.vm.filter).toBe("name:'foobar'");
|
||||
});
|
||||
|
||||
it("should have an effective filter that combines implicit and explicit filter", async () => {
|
||||
wrapper.vm.implicitFilter = "tag:cowdog";
|
||||
wrapper.vm.appendTagFilter("name", "foobar");
|
||||
expect(wrapper.vm.filter).toBe("name:'foobar'");
|
||||
expect(wrapper.vm.effectiveFilter).toBe("tag:cowdog name:'foobar'");
|
||||
});
|
||||
|
||||
it("should just use implicit filter as effective if filter is empty", async () => {
|
||||
wrapper.vm.implicitFilter = "tag:cowdog";
|
||||
expect(wrapper.vm.effectiveFilter).toBe("tag:cowdog");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{ message }}
|
||||
</b-alert>
|
||||
<Heading h2 size="md" separator>Job Lock</Heading>
|
||||
<job-lock />
|
||||
<JobLock />
|
||||
<Heading h2 size="md" separator>Job Overview</Heading>
|
||||
<p>
|
||||
Below unfinished jobs are displayed (in the 'new', 'queued', 'running', or 'upload' states) and recently
|
||||
@@ -36,9 +36,7 @@
|
||||
</b-form-group>
|
||||
</b-form>
|
||||
<b-form-group description="Use strings or regular expressions to search jobs.">
|
||||
<b-input-group id="filter-regex">
|
||||
<b-form-input v-model="filter" placeholder="Type to Search" @keyup.esc.native="filter = ''" />
|
||||
</b-input-group>
|
||||
<IndexFilter v-bind="filterAttrs" id="job-search" v-model="filter" />
|
||||
</b-form-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
@@ -56,11 +54,10 @@
|
||||
</b-form>
|
||||
</transition>
|
||||
<h3 class="mb-0 h-sm">Unfinished Jobs</h3>
|
||||
<jobs-table
|
||||
<JobsTable
|
||||
v-model="jobsItemsModel"
|
||||
:fields="unfinishedJobFields"
|
||||
:items="unfinishedJobs"
|
||||
:filter="filter"
|
||||
:table-caption="runningTableCaption"
|
||||
:no-items-message="runningNoJobsMessage"
|
||||
:loading="loading"
|
||||
@@ -78,19 +75,22 @@
|
||||
:checked="allSelected"
|
||||
:value="data.item['id']"></b-form-checkbox>
|
||||
</template>
|
||||
</jobs-table>
|
||||
</JobsTable>
|
||||
|
||||
<template v-if="!showAllRunning">
|
||||
<h3 class="mb-0 h-sm">Finished Jobs</h3>
|
||||
<jobs-table
|
||||
<JobsTable
|
||||
:table-caption="finishedTableCaption"
|
||||
:fields="finishedJobFields"
|
||||
:items="finishedJobs"
|
||||
:filter="filter"
|
||||
:no-items-message="finishedNoJobsMessage"
|
||||
:loading="loading"
|
||||
:busy="busy">
|
||||
</jobs-table>
|
||||
:busy="busy"
|
||||
@tool-clicked="(toolId) => appendTagFilter('tool', toolId)"
|
||||
@runner-clicked="(runner) => appendTagFilter('runner', runner)"
|
||||
@handler-clicked="(handler) => appendTagFilter('handler', handler)"
|
||||
@user-clicked="(user) => appendTagFilter('user', user)">
|
||||
</JobsTable>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -105,14 +105,42 @@ import { commonJobFields } from "./JobFields";
|
||||
import { errorMessageAsString } from "utils/simple-error";
|
||||
import { jobsProvider } from "components/providers/JobProvider";
|
||||
import Heading from "components/Common/Heading";
|
||||
import filtersMixin from "components/Indices/filtersMixin";
|
||||
|
||||
function cancelJob(jobId, message) {
|
||||
const url = `${getAppRoot()}api/jobs/${jobId}`;
|
||||
return axios.delete(url, { data: { message: message } });
|
||||
}
|
||||
|
||||
const helpHtml = `<div>
|
||||
<p>This textbox box can be used to filter the jobs displayed.
|
||||
|
||||
<p>Text entered here will be searched against job user, tool ID, job runner, and handler. Additionally,
|
||||
advanced filtering tags can be used to refine the search more precisely. Tags are of the form
|
||||
<code><tag_name>:<tag_value></code> or <code><tag_name>:'<tag_value>'</code>.
|
||||
For instance to search just for jobs with <code>cat1</code> in the tool name, <code>tool:cat1</code> can be used.
|
||||
Notice by default the search is not case-sensitive.
|
||||
|
||||
<p>If the quoted version of tag is used, the search is case sensitive and only full matches will be
|
||||
returned. So <code>tool:'cat1'</code> would show only jobs from the <code>cat1</code> tool exactly.</p>
|
||||
|
||||
<p>The available tags are:
|
||||
<dl>
|
||||
<dt><code>user</code></dt>
|
||||
<dd>This filters the job index to contain only jobs executed by matching user(s). You may also just click on a user in the list of jobs to filter on that exact user using this directly.</dd>
|
||||
<dt><code>handler</code></dt>
|
||||
<dd>This filters the job index to contain only jobs executed on matching handler(s). You may also just click on a handler in the list of jobs to filter on that exact user using this directly.</dd>
|
||||
<dt><code>runner</code></dt>
|
||||
<dd>This filters the job index to contain only jobs executed on matching job runner(s). You may also just click on a runner in the list of jobs to filter on that exact user using this directly.</dd>
|
||||
<dt><code>tool</code></dt>
|
||||
<dd>This filters the job index to contain only jobs from the matching tool(s). You may also just click on a tool in the list of jobs to filter on that exact user using this directly.</dd>
|
||||
</dl>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default {
|
||||
components: { JobLock, JobsTable, Heading },
|
||||
mixins: [filtersMixin],
|
||||
data() {
|
||||
return {
|
||||
jobs: [],
|
||||
@@ -130,13 +158,14 @@ export default {
|
||||
allSelected: false,
|
||||
indeterminate: false,
|
||||
stopMessage: "",
|
||||
filter: "",
|
||||
message: "",
|
||||
status: "info",
|
||||
loading: true,
|
||||
busy: true,
|
||||
cutoffMin: 5,
|
||||
showAllRunning: false,
|
||||
titleSearch: `search jobs`,
|
||||
helpHtml: helpHtml,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -205,6 +234,9 @@ export default {
|
||||
const dateRangeMin = new Date(Date.now() - cutoff * 60 * 1000).toISOString();
|
||||
params.date_range_min = `${dateRangeMin}`;
|
||||
}
|
||||
if (this.filter) {
|
||||
params.search = this.filter;
|
||||
}
|
||||
const ctx = {
|
||||
root: getAppRoot(),
|
||||
...params,
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="jobs-table-wrapper">
|
||||
<b-table
|
||||
v-model="innerValue"
|
||||
:fields="fields"
|
||||
:items="items"
|
||||
:filter="filter"
|
||||
hover
|
||||
responsive
|
||||
no-sort-reset
|
||||
striped
|
||||
caption-top
|
||||
:busy="busy"
|
||||
class="jobs-table"
|
||||
show-empty
|
||||
@row-clicked="toggleDetails">
|
||||
<template v-slot:table-caption>
|
||||
@@ -23,10 +23,39 @@
|
||||
</b-alert>
|
||||
</template>
|
||||
<template v-slot:cell(update_time)="data">
|
||||
<utc-date :date="data.value" mode="elapsed" />
|
||||
<UtcDate :date="data.value" mode="elapsed" />
|
||||
</template>
|
||||
<template v-slot:row-details="row">
|
||||
<job-details :job="row.item" />
|
||||
<JobDetails :job="row.item" />
|
||||
</template>
|
||||
<template v-slot:cell(user_email)="data">
|
||||
<a class="job-filter-link-user" :data-user="data.value" @click="$emit('user-clicked', data.value)">{{
|
||||
data.value
|
||||
}}</a>
|
||||
</template>
|
||||
<template v-slot:cell(tool_id)="data">
|
||||
<a
|
||||
class="job-filter-link-tool-id"
|
||||
:data-tool-id="data.value"
|
||||
@click="$emit('tool-clicked', data.value)"
|
||||
>{{ data.value }}</a
|
||||
>
|
||||
</template>
|
||||
<template v-slot:cell(job_runner_name)="data">
|
||||
<a
|
||||
class="job-filter-link-runner"
|
||||
:data-runner="data.value"
|
||||
@click="$emit('runner-clicked', data.value)"
|
||||
>{{ data.value }}</a
|
||||
>
|
||||
</template>
|
||||
<template v-slot:cell(handler)="data">
|
||||
<a
|
||||
class="job-filter-link-handler"
|
||||
:data-handler="data.handler"
|
||||
@click="$emit('handler-clicked', data.value)"
|
||||
>{{ data.value }}</a
|
||||
>
|
||||
</template>
|
||||
<template v-for="(index, name) in $slots" v-slot:[name]>
|
||||
<slot :name="name" />
|
||||
@@ -56,10 +85,6 @@ export default {
|
||||
items: {
|
||||
required: true,
|
||||
},
|
||||
filter: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
busy: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
||||
@@ -14,7 +14,7 @@ import DisplayApplications from "components/admin/DisplayApplications";
|
||||
import ErrorStack from "components/admin/ErrorStack";
|
||||
import FormGeneric from "components/Form/FormGeneric";
|
||||
import Grid from "components/Grid/Grid";
|
||||
import Jobs from "components/admin/Jobs";
|
||||
import JobsList from "components/admin/JobsList";
|
||||
import RegisterForm from "components/Login/RegisterForm";
|
||||
import ResetMetadata from "components/admin/ResetMetadata";
|
||||
import SanitizeAllow from "components/admin/SanitizeAllow";
|
||||
@@ -42,7 +42,7 @@ export default [
|
||||
{ path: "display_applications", component: DisplayApplications },
|
||||
{ path: "error_stack", component: ErrorStack },
|
||||
{ path: "invocations", component: ActiveInvocations },
|
||||
{ path: "jobs", component: Jobs },
|
||||
{ path: "jobs", component: JobsList },
|
||||
{ path: "reset_metadata", component: ResetMetadata },
|
||||
{ path: "sanitize_allow", component: SanitizeAllow },
|
||||
{ path: "toolbox_dependencies", component: ToolboxDependencies },
|
||||
|
||||
@@ -68,7 +68,9 @@ masthead:
|
||||
|
||||
# Shared data
|
||||
libraries: 'Data Libraries'
|
||||
published_workflows: 'Workflows'
|
||||
published_histories: 'Histories'
|
||||
published_pages: 'Pages'
|
||||
|
||||
preferences:
|
||||
selectors:
|
||||
@@ -480,6 +482,10 @@ pages:
|
||||
create: '.manage-table-actions .action-button'
|
||||
submit: '#submit'
|
||||
export: '.markdown-pdf-export'
|
||||
dropdown: '[data-page-dropdown*="${id}"]'
|
||||
index_table: "#page-table"
|
||||
index_rows: "#page-table > tbody > tr:not(.b-table-empty-row, [style*='display: none'])"
|
||||
|
||||
editor:
|
||||
selectors:
|
||||
save: '#save-button'
|
||||
@@ -544,6 +550,7 @@ workflows:
|
||||
pager_page_last: '.gx-workflows-grid-pager .gx-grid-pager-last button'
|
||||
pager_page_previous: '.gx-workflows-grid-pager .gx-grid-pager-prev button'
|
||||
pager_page_active: '.gx-workflows-grid-pager .gx-grid-pager-page.active button'
|
||||
dropdown: '[data-workflow-dropdown*="${id}"]'
|
||||
run_button: '[data-workflow-run*="${id}"]'
|
||||
bookmark_link: '.workflow-bookmark-link'
|
||||
workflow_with_name:
|
||||
@@ -783,6 +790,16 @@ admin:
|
||||
type: xpath
|
||||
selector: "//label[@for='prevent-job-dispatching']/strong"
|
||||
cutoff: '#cutoff'
|
||||
table: '.jobs-table'
|
||||
filter_link_user: '.job-filter-link-user'
|
||||
filter_link_tool: '.job-filter-link-tool-id'
|
||||
filter_link_handler: '.job-filter-link-handler'
|
||||
filter_link_runner: '.job-filter-link-runner'
|
||||
filter_link_by_user: '.job-filter-link-user[data-user="${user}"]'
|
||||
filter_link_by_tool: '.job-filter-link-tool-id[data-tool-id="${tool_id}"]'
|
||||
filter_link_by_handler: '.job-filter-link-handler[data-handler="${handler}"]'
|
||||
filter_link_by_runner: '.job-filter-link-runner[data-runner="${runner}"]'
|
||||
filter_query: '.index-filter-query'
|
||||
|
||||
toolshed:
|
||||
selectors:
|
||||
|
||||
@@ -234,7 +234,10 @@ class NavigatesGalaxy(HasDriver):
|
||||
def home(self) -> None:
|
||||
"""Return to root Galaxy page and wait for some basic widgets to appear."""
|
||||
self.get()
|
||||
self.components.masthead._.wait_for_visible()
|
||||
try:
|
||||
self.components.masthead._.wait_for_visible()
|
||||
except SeleniumTimeoutException as e:
|
||||
raise ClientBuildException(e)
|
||||
|
||||
def go_to_trs_search(self) -> None:
|
||||
self.driver.get(self.build_url("workflows/trs_search"))
|
||||
@@ -1080,6 +1083,21 @@ class NavigatesGalaxy(HasDriver):
|
||||
self.click_masthead_user()
|
||||
self.components.masthead.pages.wait_for_and_click()
|
||||
|
||||
def navigate_to_published_workflows(self):
|
||||
self.home()
|
||||
self.click_masthead_shared_data()
|
||||
self.components.masthead.published_workflows.wait_for_and_click()
|
||||
|
||||
def navigate_to_published_histories_page(self):
|
||||
self.home()
|
||||
self.click_masthead_shared_data()
|
||||
self.components.masthead.published_histories.wait_for_and_click()
|
||||
|
||||
def navigate_to_published_pages(self):
|
||||
self.home()
|
||||
self.click_masthead_shared_data()
|
||||
self.components.masthead.published_pages.wait_for_and_click()
|
||||
|
||||
def admin_open(self):
|
||||
self.components.masthead.admin.wait_for_and_click()
|
||||
|
||||
@@ -1241,6 +1259,16 @@ class NavigatesGalaxy(HasDriver):
|
||||
action_chains.move_to_element(center_element).perform()
|
||||
self.wait_for_selector_absent_or_hidden(".b-tooltip", wait_type=WAIT_TYPES.UX_POPUP)
|
||||
|
||||
def pages_index_table_elements(self):
|
||||
pages = self.components.pages
|
||||
pages.index_table.wait_for_visible()
|
||||
return pages.index_rows.all()
|
||||
|
||||
def page_index_click_option(self, option_title, page_id):
|
||||
self.components.pages.dropdown(id=page_id).wait_for_and_click()
|
||||
if not self.select_dropdown_item(option_title):
|
||||
raise AssertionError(f"Failed to find page action option with title [{option_title}]")
|
||||
|
||||
def workflow_index_open(self):
|
||||
self.home()
|
||||
self.click_masthead_workflow()
|
||||
@@ -2065,3 +2093,9 @@ class NotLoggedInException(SeleniumTimeoutException):
|
||||
template = "Waiting for UI to reflect user logged in but it did not occur. API indicates no user is currently logged in. %s API response was [%s]. %s"
|
||||
msg = template % (dom_message, user_info, timeout_exception.msg)
|
||||
super().__init__(msg=msg, screen=timeout_exception.screen, stacktrace=timeout_exception.stacktrace)
|
||||
|
||||
|
||||
class ClientBuildException(SeleniumTimeoutException):
|
||||
def __init__(self, timeout_exception: SeleniumTimeoutException):
|
||||
msg = f"Error waiting for Galaxy masthead to appear, this frequently means there is a problem with the client build and the Galaxy client is broken. {timeout_exception.msg}"
|
||||
super().__init__(msg=msg, screen=timeout_exception.screen, stacktrace=timeout_exception.stacktrace)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import os
|
||||
|
||||
from galaxy_test.base.decorators import requires_admin
|
||||
from galaxy_test.base.populators import skip_without_tool
|
||||
from galaxy_test.base.workflow_fixtures import (
|
||||
WORKFLOW_WITH_CUSTOM_REPORT_1,
|
||||
WORKFLOW_WITH_CUSTOM_REPORT_1_TEST_DATA,
|
||||
)
|
||||
from galaxy_test.selenium.framework import retry_assertion_during_transitions
|
||||
from .framework import (
|
||||
selenium_test,
|
||||
SeleniumIntegrationTestCase,
|
||||
)
|
||||
|
||||
SCRIPT_DIRECTORY = os.path.abspath(os.path.dirname(__file__))
|
||||
JOB_CONFIG = os.path.join(SCRIPT_DIRECTORY, "test_admin_jobs_job_conf.yml")
|
||||
|
||||
|
||||
class TestAdminJobsGrid(SeleniumIntegrationTestCase):
|
||||
run_as_admin = True
|
||||
|
||||
@classmethod
|
||||
def handle_galaxy_config_kwds(cls, config):
|
||||
super().handle_galaxy_config_kwds(config)
|
||||
config["job_config_file"] = JOB_CONFIG
|
||||
|
||||
@selenium_test
|
||||
@requires_admin
|
||||
@skip_without_tool("cat")
|
||||
def test_jobs_grid(self):
|
||||
admin_component = self.components.admin
|
||||
self.admin_login()
|
||||
with self.dataset_populator.test_history() as history_id:
|
||||
run_object = self.workflow_populator.run_workflow(
|
||||
WORKFLOW_WITH_CUSTOM_REPORT_1, test_data=WORKFLOW_WITH_CUSTOM_REPORT_1_TEST_DATA, history_id=history_id
|
||||
)
|
||||
self.workflow_populator.wait_for_invocation_and_jobs(
|
||||
history_id, run_object.workflow_id, run_object.invocation_id
|
||||
)
|
||||
self.admin_open()
|
||||
admin_component.index.jobs.wait_for_and_click()
|
||||
self.sleep_for(self.wait_types.UX_RENDER)
|
||||
manage_jobs = admin_component.manage_jobs
|
||||
manage_jobs.table.wait_for_visible()
|
||||
self.screenshot("admin_jobs_custom_configuration")
|
||||
self.sleep_for(self.wait_types.JOB_COMPLETION)
|
||||
assert len(manage_jobs.filter_link_by_tool(tool_id="cat").all()) == 2
|
||||
assert len(manage_jobs.filter_link_by_tool(tool_id="qc_stdout").all()) == 1
|
||||
self.assert_displays_at_least_n_rows(3)
|
||||
|
||||
manage_jobs.filter_link_by_tool(tool_id="qc_stdout").wait_for_and_click()
|
||||
assert manage_jobs.filter_query.wait_for_value() == "tool:'qc_stdout'"
|
||||
self.assert_displays_n_rows(1)
|
||||
|
||||
self.clear_index_filter()
|
||||
self.assert_displays_at_least_n_rows(3)
|
||||
|
||||
manage_jobs.filter_link_by_runner(runner="local_cat").wait_for_and_click()
|
||||
self.assert_displays_n_rows(2)
|
||||
self.clear_index_filter()
|
||||
|
||||
@retry_assertion_during_transitions
|
||||
@requires_admin
|
||||
def assert_displays_at_least_n_rows(self, n):
|
||||
admin_component = self.components.admin
|
||||
manage_jobs = admin_component.manage_jobs
|
||||
assert len(manage_jobs.filter_link_user.all()) >= n
|
||||
|
||||
@retry_assertion_during_transitions
|
||||
@requires_admin
|
||||
def assert_displays_n_rows(self, n):
|
||||
admin_component = self.components.admin
|
||||
manage_jobs = admin_component.manage_jobs
|
||||
assert len(manage_jobs.filter_link_user.all()) == n
|
||||
|
||||
@requires_admin
|
||||
def clear_index_filter(self):
|
||||
admin_component = self.components.admin
|
||||
manage_jobs = admin_component.manage_jobs
|
||||
# clear alone doesn't fire the debounce input...
|
||||
element = manage_jobs.filter_query.wait_for_visible()
|
||||
element.clear()
|
||||
element.send_keys(" ")
|
||||
self.send_enter(element)
|
||||
@@ -0,0 +1,24 @@
|
||||
runners:
|
||||
local_qc:
|
||||
load: galaxy.jobs.runners.local:LocalJobRunner
|
||||
workers: 1
|
||||
local_cat:
|
||||
load: galaxy.jobs.runners.local:LocalJobRunner
|
||||
workers: 1
|
||||
local_upload:
|
||||
load: galaxy.jobs.runners.local:LocalJobRunner
|
||||
workers: 1
|
||||
execution:
|
||||
default: local_upload
|
||||
environments:
|
||||
local_qc:
|
||||
runner: local_qc
|
||||
local_cat:
|
||||
runner: local_cat
|
||||
local_upload:
|
||||
runner: local_upload
|
||||
tools:
|
||||
- id: cat
|
||||
environment: local_cat
|
||||
- id: qc_stdout
|
||||
environment: local_qc
|
||||
Reference in New Issue
Block a user