Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement admin jobs filtering #16020

Merged
merged 2 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion client/src/components/Indices/filtersMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,39 @@ export default {
components: {
IndexFilter,
},
props: {
martenson marked this conversation as resolved.
Show resolved Hide resolved
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: {
Expand Down
12 changes: 12 additions & 0 deletions client/src/components/Indices/filtersMixin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand All @@ -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"
Expand All @@ -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>
Expand All @@ -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>&lt;tag_name&gt;:&lt;tag_value&gt;</code> or <code>&lt;tag_name&gt;:'&lt;tag_value&gt;'</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: [],
Expand All @@ -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: {
Expand Down Expand Up @@ -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,
Expand Down
41 changes: 33 additions & 8 deletions client/src/components/admin/JobsTable.vue
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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" />
Expand Down Expand Up @@ -56,10 +85,6 @@ export default {
items: {
required: true,
},
filter: {
type: String,
required: true,
},
busy: {
type: Boolean,
required: true,
Expand Down
4 changes: 2 additions & 2 deletions client/src/entry/analysis/routes/admin-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 },
Expand Down
17 changes: 17 additions & 0 deletions client/src/utils/navigation/navigation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ masthead:

# Shared data
libraries: 'Data Libraries'
published_workflows: 'Workflows'
published_histories: 'Histories'
published_pages: 'Pages'

preferences:
selectors:
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -540,6 +546,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:
Expand Down Expand Up @@ -779,6 +786,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:
Expand Down
36 changes: 35 additions & 1 deletion lib/galaxy/selenium/navigates_galaxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,10 @@ def timeout_for(self, wait_type: WaitType = DEFAULT_WAIT_TYPE, **kwd) -> float:
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"))
Expand Down Expand Up @@ -1080,6 +1083,21 @@ def navigate_to_pages(self):
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()

Expand Down Expand Up @@ -1241,6 +1259,16 @@ def clear_tooltips(self):
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()
Expand Down Expand Up @@ -2065,3 +2093,9 @@ def __init__(self, timeout_exception, user_info, dom_message):
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)
Loading