mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 16:00:38 +00:00
feat: cancel jobs based on filters (#3874)
* WIP * Add endpoint to cancel jobs * Fix endpoints and add modal to cancel jobs * Add useful tooltips and warnings * Fix openapi.yml * Prepare sqlx * Prepare sqlx * Select jobs to cancel * Prepare sqlx * Remove unused variable * Make small fixes - gap between buttons - dropdown in one button with chevron instead of two buttons - filters shown on the cancel filtered modal
This commit is contained in:
+7
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, running, is_flow_step FROM queue WHERE scheduled_for < now() AND workspace_id = $1 AND schedule_path IS NULL",
|
||||
"query": "SELECT id, is_flow_step, running FROM queue WHERE id = ANY($1) AND schedule_path IS NULL",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -10,25 +10,25 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "running",
|
||||
"name": "is_flow_step",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "is_flow_step",
|
||||
"name": "running",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
"UuidArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "caeb49629b8673c1f1c84a6e40c3e2d2c3bc3fdbde530a0a6b6fd68a22b867c3"
|
||||
"hash": "4c529e29a0eb084a92f4e952506d701592bc707fd8a8cb3311bf146be6078f26"
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
|
||||
|
||||
@@ -5418,14 +5418,75 @@ paths:
|
||||
required:
|
||||
- database_length
|
||||
|
||||
/w/{workspace}/jobs/queue/cancel_all:
|
||||
post:
|
||||
summary: cancel all jobs
|
||||
operationId: cancelAll
|
||||
/w/{workspace}/jobs/queue/list_filtered_uuids:
|
||||
get:
|
||||
summary: get the ids of all jobs matching the given filters
|
||||
operationId: listFilteredUuids
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/OrderDesc"
|
||||
- $ref: "#/components/parameters/CreatedBy"
|
||||
- $ref: "#/components/parameters/ParentJob"
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
- $ref: "#/components/parameters/SchedulePath"
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/Success"
|
||||
- $ref: "#/components/parameters/ScheduledForBeforeNow"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/Suspended"
|
||||
- $ref: "#/components/parameters/Running"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- $ref: "#/components/parameters/Tag"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- name: concurrency_key
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: all_workspaces
|
||||
description: get jobs from all workspaces (only valid if request come from the `admins` workspace)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: is_not_schedule
|
||||
description: is not a scheduled job
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: uuids of jobs
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/jobs/queue/cancel_selection:
|
||||
post:
|
||||
summary: cancel jobs based on the given uuids
|
||||
operationId: cancelSelection
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
description: uuids of the jobs to cancel
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: uuids of canceled jobs
|
||||
|
||||
@@ -120,11 +120,10 @@ struct ObscuredJob {
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct ExtendedJobsParams {
|
||||
concurrency_key: Option<String>,
|
||||
row_limit: Option<i64>,
|
||||
}
|
||||
|
||||
fn join_concurrency_key<'c>(concurrency_key: Option<&String>, mut sqlb: SqlBuilder) -> SqlBuilder {
|
||||
pub fn join_concurrency_key<'c>(concurrency_key: Option<&String>, mut sqlb: SqlBuilder) -> SqlBuilder {
|
||||
if let Some(key) = concurrency_key {
|
||||
sqlb.join("concurrency_key")
|
||||
.on_eq("id", "concurrency_key.job_id")
|
||||
@@ -151,7 +150,6 @@ async fn get_concurrent_intervals(
|
||||
}
|
||||
|
||||
let row_limit = iq.row_limit.unwrap_or(1000);
|
||||
let concurrency_key = iq.concurrency_key;
|
||||
|
||||
let lq = ListCompletedQuery { order_desc: Some(true), ..lq };
|
||||
let lqc = lq.clone();
|
||||
@@ -177,10 +175,10 @@ async fn get_concurrent_intervals(
|
||||
.limit(row_limit)
|
||||
.clone();
|
||||
|
||||
sqlb_q = join_concurrency_key(concurrency_key.as_ref(), sqlb_q);
|
||||
sqlb_c = join_concurrency_key(concurrency_key.as_ref(), sqlb_c);
|
||||
sqlb_q_user = join_concurrency_key(concurrency_key.as_ref(), sqlb_q_user);
|
||||
sqlb_c_user = join_concurrency_key(concurrency_key.as_ref(), sqlb_c_user);
|
||||
sqlb_q = join_concurrency_key(lq.concurrency_key.as_ref(), sqlb_q);
|
||||
sqlb_c = join_concurrency_key(lq.concurrency_key.as_ref(), sqlb_c);
|
||||
sqlb_q_user = join_concurrency_key(lq.concurrency_key.as_ref(), sqlb_q_user);
|
||||
sqlb_c_user = join_concurrency_key(lq.concurrency_key.as_ref(), sqlb_c_user);
|
||||
|
||||
let should_fetch_obscured_jobs = match lq {
|
||||
ListCompletedQuery {
|
||||
@@ -211,7 +209,8 @@ async fn get_concurrent_intervals(
|
||||
job_kinds: _,
|
||||
is_flow_step: _,
|
||||
all_workspaces: _,
|
||||
} => concurrency_key.is_some(),
|
||||
concurrency_key: Some(_),
|
||||
} => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ use windmill_common::worker::TMP_DIR;
|
||||
use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH;
|
||||
use windmill_common::variables::get_workspace_key;
|
||||
|
||||
use crate::concurrency_groups::join_concurrency_key;
|
||||
use crate::add_webhook_allowed_origin;
|
||||
use crate::db::ApiAuthed;
|
||||
|
||||
@@ -191,7 +192,8 @@ pub fn workspaced_service() -> Router {
|
||||
)
|
||||
.route("/queue/list", get(list_queue_jobs))
|
||||
.route("/queue/count", get(count_queue_jobs))
|
||||
.route("/queue/cancel_all", post(cancel_all))
|
||||
.route("/queue/list_filtered_uuids", get(list_filtered_uuids))
|
||||
.route("/queue/cancel_selection", post(cancel_selection))
|
||||
.route("/completed/count", get(count_completed_jobs))
|
||||
.route(
|
||||
"/completed/list",
|
||||
@@ -994,6 +996,7 @@ pub struct ListQueueQuery {
|
||||
pub is_flow_step: Option<bool>,
|
||||
pub has_null_parent: Option<bool>,
|
||||
pub is_not_schedule: Option<bool>,
|
||||
pub concurrency_key: Option<String>,
|
||||
}
|
||||
|
||||
impl From<ListCompletedQuery> for ListQueueQuery {
|
||||
@@ -1022,6 +1025,7 @@ impl From<ListCompletedQuery> for ListQueueQuery {
|
||||
is_flow_step: lcq.is_flow_step,
|
||||
has_null_parent: lcq.has_null_parent,
|
||||
is_not_schedule: lcq.is_not_schedule,
|
||||
concurrency_key: lcq.concurrency_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1217,23 +1221,20 @@ async fn list_queue_jobs(
|
||||
Ok(Json(jobs))
|
||||
}
|
||||
|
||||
async fn cancel_all(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
#[derive(Deserialize, FromRow)]
|
||||
struct JobToCancel {
|
||||
id: Uuid,
|
||||
is_flow_step: Option<bool>,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
Path(w_id): Path<String>,
|
||||
async fn cancel_jobs(
|
||||
jobs: Vec<JobToCancel>,
|
||||
db: &DB,
|
||||
username: &str,
|
||||
w_id: &str,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
) -> error::JsonResult<Vec<Uuid>> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let jobs = sqlx::query!(
|
||||
"SELECT id, running, is_flow_step FROM queue WHERE scheduled_for < now() AND workspace_id = $1 AND schedule_path IS NULL",
|
||||
w_id,
|
||||
)
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
let username = authed.username;
|
||||
let mut uuids = vec![];
|
||||
for j in jobs.iter() {
|
||||
let r = sqlx::query!(
|
||||
@@ -1241,7 +1242,7 @@ async fn cancel_all(
|
||||
username,
|
||||
j.id,
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.fetch_optional(db)
|
||||
.await;
|
||||
|
||||
if r.as_ref().is_ok_and(|x| x.is_some()) {
|
||||
@@ -1254,7 +1255,7 @@ async fn cancel_all(
|
||||
if let Some(job_running) = job_running {
|
||||
append_logs(
|
||||
&j.id,
|
||||
w_id.clone(),
|
||||
w_id,
|
||||
format!("canceled by {username}: cancel_all"),
|
||||
db.clone(),
|
||||
)
|
||||
@@ -1286,6 +1287,62 @@ async fn cancel_all(
|
||||
Ok(Json(uuids))
|
||||
}
|
||||
|
||||
async fn cancel_selection(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
|
||||
|
||||
Path(w_id): Path<String>,
|
||||
Json(jobs): Json<Vec<Uuid>>,
|
||||
) -> error::JsonResult<Vec<Uuid>> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let jobs_to_cancel = sqlx::query_as!(
|
||||
JobToCancel,
|
||||
"SELECT id, is_flow_step, running FROM queue WHERE id = ANY($1) AND schedule_path IS NULL",
|
||||
&jobs
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
cancel_jobs(
|
||||
jobs_to_cancel,
|
||||
&db,
|
||||
authed.username.as_str(),
|
||||
w_id.as_str(),
|
||||
rsmq,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_filtered_uuids(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
|
||||
Path(w_id): Path<String>,
|
||||
Query(lq): Query<ListQueueQuery>,
|
||||
) -> error::JsonResult<Vec<Uuid>> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mut sqlb = SqlBuilder::select_from("queue")
|
||||
.fields(&["id"])
|
||||
.clone();
|
||||
|
||||
sqlb = join_concurrency_key(lq.concurrency_key.as_ref(), sqlb);
|
||||
|
||||
sqlb.and_where_is_null("schedule_path");
|
||||
|
||||
sqlb = filter_list_queue_query(sqlb, &lq, w_id.as_str(), false);
|
||||
|
||||
let sql = sqlb.query()?;
|
||||
let jobs = sqlx::query_scalar(sql.as_str()).fetch_all(&db).await?;
|
||||
|
||||
Ok(Json(jobs))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, FromRow)]
|
||||
struct QueueStats {
|
||||
database_length: i64,
|
||||
@@ -4326,6 +4383,7 @@ pub struct ListCompletedQuery {
|
||||
pub has_null_parent: Option<bool>,
|
||||
pub label: Option<String>,
|
||||
pub is_not_schedule: Option<bool>,
|
||||
pub concurrency_key: Option<String>,
|
||||
}
|
||||
|
||||
async fn list_completed_jobs(
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
let c: string = ''
|
||||
export { c as class }
|
||||
export let style = ''
|
||||
export let cancelText: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -87,7 +88,9 @@
|
||||
color="light"
|
||||
size="sm"
|
||||
>
|
||||
<span class="inline-flex gap-2">Cancel <Badge color="dark-gray">Escape</Badge></span>
|
||||
<span class="inline-flex gap-2"
|
||||
>{cancelText ?? 'Cancel'}<Badge color="dark-gray">Escape</Badge></span
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</script>
|
||||
|
||||
<Popover notClickable>
|
||||
<svete:frament slot="text">
|
||||
<svelte:fragment slot="text">
|
||||
<div class="mb-5">
|
||||
{#if self_wait_time_ms != undefined}
|
||||
<div>
|
||||
@@ -66,7 +66,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div> In a healthy queue, jobs are expected to start in under 50ms. </div>
|
||||
</svete:frament>
|
||||
</svelte:fragment>
|
||||
{#if variant === 'icon'}
|
||||
<Hourglass class={classFromColorName(waitColorTresholds(total_wait))} size={14} />
|
||||
{:else if variant === 'badge'}
|
||||
|
||||
@@ -31,12 +31,17 @@
|
||||
export let containerWidth: number = 0
|
||||
export let containsLabel: boolean = false
|
||||
export let activeLabel: string | null
|
||||
export let isSelectingJobsToCancel: boolean = false
|
||||
|
||||
let scheduleEditor: ScheduleEditor
|
||||
|
||||
$: isExternal = job && job.id === '-'
|
||||
|
||||
let triggeredByWidth: number = 0
|
||||
|
||||
function isJobCancelable(j: Job): boolean {
|
||||
return j.type === 'QueuedJob' && !j.schedule_path
|
||||
}
|
||||
</script>
|
||||
|
||||
<Portal>
|
||||
@@ -52,10 +57,17 @@
|
||||
)}
|
||||
style="width: {containerWidth}px"
|
||||
on:click={() => {
|
||||
dispatch('select')
|
||||
if (!isSelectingJobsToCancel || isJobCancelable(job)) {
|
||||
dispatch('select')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="w-1/12 flex justify-center">
|
||||
{#if isSelectingJobsToCancel && isJobCancelable(job)}
|
||||
<div class="px-2">
|
||||
<input type="checkbox" checked={selected}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isExternal}
|
||||
<Badge color="gray" baseClass="!px-1.5">
|
||||
<ShieldQuestion size={14} />
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
export let externalJobs: Job[] = []
|
||||
export let omittedObscuredJobs: boolean
|
||||
export let showExternalJobs: boolean = false
|
||||
export let isSelectingJobsToCancel: boolean = false
|
||||
export let selectedIds: string[] = []
|
||||
export let selectedWorkspace: string | undefined = undefined
|
||||
export let activeLabel: string | null = null
|
||||
@@ -171,13 +172,14 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $workspaceStore !== 'admins' && omittedObscuredJobs}
|
||||
{:else if $workspaceStore !== 'admins' && omittedObscuredJobs}
|
||||
<div class="w-1/12 text-2xs flex flex-row">
|
||||
{jobs && jobCountString(jobs.length)}
|
||||
<Popover>
|
||||
<AlertTriangle size={16} class="ml-0.5 text-yellow-500"/>
|
||||
<AlertTriangle size={16} class="ml-0.5 text-yellow-500" />
|
||||
<svelte:fragment slot="text">
|
||||
Too specific filtering may have caused the omission of obscured jobs. This is done for security reasons. To see obscured jobs, try removing some filters.
|
||||
Too specific filtering may have caused the omission of obscured jobs. This is done for
|
||||
security reasons. To see obscured jobs, try removing some filters.
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</div>
|
||||
@@ -214,10 +216,21 @@
|
||||
{containsLabel}
|
||||
job={jobOrDate.job}
|
||||
selected={jobOrDate.job.id !== '-' && selectedIds.includes(jobOrDate.job.id)}
|
||||
{isSelectingJobsToCancel}
|
||||
on:select={() => {
|
||||
selectedWorkspace = jobOrDate.job.workspace_id
|
||||
selectedIds = [jobOrDate.job.id]
|
||||
dispatch('select')
|
||||
const jobId = jobOrDate.job.id
|
||||
if (isSelectingJobsToCancel) {
|
||||
if (selectedIds.includes(jobOrDate.job.id)) {
|
||||
selectedIds = selectedIds.filter((id) => id != jobId)
|
||||
} else {
|
||||
selectedIds.push(jobId)
|
||||
selectedIds = selectedIds
|
||||
}
|
||||
} else {
|
||||
selectedWorkspace = jobOrDate.job.workspace_id
|
||||
selectedIds = [jobOrDate.job.id]
|
||||
dispatch('select')
|
||||
}
|
||||
}}
|
||||
{activeLabel}
|
||||
on:filterByLabel
|
||||
|
||||
@@ -31,10 +31,11 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ManuelDatePicker from '$lib/components/runs/ManuelDatePicker.svelte'
|
||||
import JobLoader from '$lib/components/runs/JobLoader.svelte'
|
||||
import { AlertTriangle, Calendar, Clock } from 'lucide-svelte'
|
||||
import { AlertTriangle, Calendar, Check, ChevronDown, Clock, X } from 'lucide-svelte'
|
||||
import ConcurrentJobsChart from '$lib/components/ConcurrentJobsChart.svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
|
||||
let jobs: Job[] | undefined
|
||||
let selectedIds: string[] = []
|
||||
@@ -97,7 +98,9 @@
|
||||
let selectedManualDate = 0
|
||||
let autoRefresh: boolean = true
|
||||
let runDrawer: Drawer
|
||||
let cancelAllJobs = false
|
||||
let isCancelingVisibleJobs = false
|
||||
let isCancelingFilteredJobs = false
|
||||
|
||||
let innerWidth = window.innerWidth
|
||||
let jobLoader: JobLoader | undefined = undefined
|
||||
let externalJobs: Job[] | undefined = undefined
|
||||
@@ -248,6 +251,8 @@
|
||||
completedJobs = undefined
|
||||
selectedManualDate = 0
|
||||
selectedIds = []
|
||||
jobIdsToCancel = []
|
||||
isSelectingJobsToCancel = false
|
||||
selectedWorkspace = undefined
|
||||
jobLoader?.loadJobs(minTs, maxTs, true)
|
||||
}
|
||||
@@ -327,6 +332,60 @@
|
||||
}
|
||||
}
|
||||
|
||||
let jobIdsToCancel: string[] = []
|
||||
let isSelectingJobsToCancel = false
|
||||
let fetchingFilteredJobs = false
|
||||
let selectedFiltersString: string | undefined = undefined
|
||||
|
||||
async function cancelVisibleJobs() {
|
||||
isSelectingJobsToCancel = true
|
||||
selectedIds = jobs?.filter(isJobCancelable).map((j) => j.id) ?? []
|
||||
if (selectedIds.length === 0 ) {
|
||||
sendUserToast("There are no visible jobs that can be canceled", true)
|
||||
}
|
||||
}
|
||||
async function cancelFilteredJobs() {
|
||||
isCancelingFilteredJobs = true
|
||||
fetchingFilteredJobs = true
|
||||
const selectedFilters = {
|
||||
workspace: $workspaceStore ?? '',
|
||||
startedBefore: maxTs,
|
||||
startedAfter: minTs,
|
||||
schedulePath,
|
||||
scriptPathExact: path === null || path === '' ? undefined : path,
|
||||
createdBy: user === null || user === '' ? undefined : user,
|
||||
scriptPathStart: folder === null || folder === '' ? undefined : `f/${folder}/`,
|
||||
jobKinds,
|
||||
success: success == 'success' ? true : success == 'failure' ? false : undefined,
|
||||
running: success == 'running' ? true : undefined,
|
||||
isNotSchedule: showSchedules == false ? true : undefined,
|
||||
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
|
||||
args:
|
||||
argFilter && argFilter != '{}' && argFilter != '' && argError == ''
|
||||
? argFilter
|
||||
: undefined,
|
||||
result:
|
||||
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
|
||||
? resultFilter
|
||||
: undefined,
|
||||
allWorkspaces: allWorkspaces ? true : undefined,
|
||||
concurrencyKey: concurrencyKey ?? undefined
|
||||
}
|
||||
|
||||
selectedFiltersString = JSON.stringify(selectedFilters, null, 4)
|
||||
jobIdsToCancel = await JobService.listFilteredUuids(selectedFilters)
|
||||
fetchingFilteredJobs = false
|
||||
}
|
||||
|
||||
async function cancelSelectedJobs() {
|
||||
jobIdsToCancel = selectedIds
|
||||
isCancelingVisibleJobs = true
|
||||
}
|
||||
|
||||
function isJobCancelable(j: Job): boolean {
|
||||
return j.type === 'QueuedJob' && !j.schedule_path
|
||||
}
|
||||
|
||||
const warnJobLimitMsg =
|
||||
'The exact number of concurrent job at the beginning of the time range may be incorrect as only the last 1000 jobs are taken into account: a job that was started earlier than this limit will not be taken into account'
|
||||
|
||||
@@ -334,6 +393,10 @@
|
||||
graph === 'ConcurrencyChart' &&
|
||||
extendedJobs !== undefined &&
|
||||
extendedJobs.jobs.length + extendedJobs.obscured_jobs.length >= 1000
|
||||
|
||||
$: if (selectedIds.length === 0) {
|
||||
isSelectingJobsToCancel = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<JobLoader
|
||||
@@ -368,17 +431,45 @@
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
title="Confirm cancelling all jobs"
|
||||
confirmationText="Cancel all jobs"
|
||||
open={cancelAllJobs}
|
||||
title={`Confirm cancelling all jobs correspoding to the selected filters (${jobIdsToCancel.length} jobs)`}
|
||||
confirmationText={`Cancel ${jobIdsToCancel.length} jobs that matched the filters`}
|
||||
open={isCancelingFilteredJobs}
|
||||
on:confirmed={async () => {
|
||||
cancelAllJobs = false
|
||||
let uuids = await JobService.cancelAll({ workspace: $workspaceStore ?? '' })
|
||||
isCancelingFilteredJobs = false
|
||||
let uuids = await JobService.cancelSelection({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: jobIdsToCancel
|
||||
})
|
||||
jobIdsToCancel = []
|
||||
selectedIds = []
|
||||
jobLoader?.loadJobs(minTs, maxTs, true, true)
|
||||
sendUserToast(`Canceled ${uuids.length} jobs`)
|
||||
}}
|
||||
loading={fetchingFilteredJobs}
|
||||
on:canceled={() => {
|
||||
isCancelingFilteredJobs = false
|
||||
}}
|
||||
>
|
||||
<pre>{selectedFiltersString}</pre>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
title={`Confirm cancelling the jobs visible on this page`}
|
||||
confirmationText={`Cancel ${jobIdsToCancel.length} jobs`}
|
||||
open={isCancelingVisibleJobs}
|
||||
on:confirmed={async () => {
|
||||
isCancelingVisibleJobs = false
|
||||
let uuids = await JobService.cancelSelection({
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: jobIdsToCancel
|
||||
})
|
||||
jobIdsToCancel = []
|
||||
selectedIds = []
|
||||
jobLoader?.loadJobs(minTs, maxTs, true, true)
|
||||
sendUserToast(`Canceled ${uuids.length} jobs`)
|
||||
}}
|
||||
on:canceled={() => {
|
||||
cancelAllJobs = false
|
||||
isCancelingVisibleJobs = false
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -492,14 +583,65 @@
|
||||
<div class="flex flex-col gap-1 md:flex-row w-full p-4">
|
||||
<div class="flex gap-2 grow flex-row">
|
||||
<RunsQueue {queue_count} {allWorkspaces} />
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="contained"
|
||||
title="Require to be an admin. Cancel all jobs in queue"
|
||||
disabled={!$userStore?.is_admin && !$superadmin}
|
||||
on:click={async () => (cancelAllJobs = true)}>Cancel All</Button
|
||||
>
|
||||
<div class="flex flex-row">
|
||||
{#if isSelectingJobsToCancel}
|
||||
<div class="mt-1 p-2 h-8 flex flex-row items-center gap-1">
|
||||
<Button
|
||||
startIcon={{ icon: Check }}
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="contained"
|
||||
on:click={cancelSelectedJobs}
|
||||
/>
|
||||
<Button
|
||||
startIcon={{ icon: X }}
|
||||
size="xs"
|
||||
color="gray"
|
||||
variant="contained"
|
||||
on:click={() => {
|
||||
isSelectingJobsToCancel = false
|
||||
selectedIds = []
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if !$userStore?.is_admin && !$superadmin}
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: 'Select jobs to cancel',
|
||||
action: cancelVisibleJobs
|
||||
}
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<div
|
||||
class="mt-1 p-2 h-8 flex flex-row items-center hover:bg-surface-hover cursor-pointer rounded-md"
|
||||
>
|
||||
<span class="text-xs min-w-[5rem]">Cancel jobs</span>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</DropdownV2>
|
||||
{:else}
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: 'Select jobs to cancel',
|
||||
action: cancelVisibleJobs
|
||||
},
|
||||
{ displayName: 'Cancel all jobs matching filters', action: cancelFilteredJobs }
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<div
|
||||
class="mt-1 p-2 h-8 flex flex-row items-center hover:bg-surface-hover cursor-pointer rounded-md"
|
||||
>
|
||||
<span class="text-xs min-w-[5rem]">Cancel jobs</span>
|
||||
<ChevronDown class="w-5 h-5" />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</DropdownV2>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex gap-2 items-center pr-8 w-40">
|
||||
<Toggle
|
||||
@@ -603,6 +745,7 @@
|
||||
omittedObscuredJobs={extendedJobs?.omitted_obscured_jobs ?? false}
|
||||
showExternalJobs={!graphIsRunsChart}
|
||||
activeLabel={label}
|
||||
{isSelectingJobsToCancel}
|
||||
bind:selectedIds
|
||||
bind:selectedWorkspace
|
||||
on:filterByPath={filterByPath}
|
||||
@@ -724,14 +867,65 @@
|
||||
{#if queue_count}
|
||||
<RunsQueue {queue_count} {allWorkspaces} />
|
||||
{/if}
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="contained"
|
||||
title="Require to be an admin. Cancel all jobs in queue"
|
||||
disabled={!$userStore?.is_admin && !$superadmin}
|
||||
on:click={async () => (cancelAllJobs = true)}>Cancel All</Button
|
||||
>
|
||||
<div class="flex flex-row">
|
||||
{#if isSelectingJobsToCancel}
|
||||
<div class="mt-1 p-2 h-8 flex flex-row items-center gap-1">
|
||||
<Button
|
||||
startIcon={{ icon: Check }}
|
||||
size="xs"
|
||||
color="red"
|
||||
variant="contained"
|
||||
on:click={cancelSelectedJobs}
|
||||
/>
|
||||
<Button
|
||||
startIcon={{ icon: X }}
|
||||
size="xs"
|
||||
color="gray"
|
||||
variant="contained"
|
||||
on:click={() => {
|
||||
isSelectingJobsToCancel = false
|
||||
selectedIds = []
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if !$userStore?.is_admin && !$superadmin}
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: 'Select jobs to cancel',
|
||||
action: cancelVisibleJobs
|
||||
}
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<div
|
||||
class="mt-1 p-2 h-8 flex flex-row items-center hover:bg-surface-hover cursor-pointer rounded-md"
|
||||
>
|
||||
<span class="text-xs min-w-[5rem]">Cancel jobs</span>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</DropdownV2>
|
||||
{:else}
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: 'Select jobs to cancel',
|
||||
action: cancelVisibleJobs
|
||||
},
|
||||
{ displayName: 'Cancel all jobs matching filters', action: cancelFilteredJobs }
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<div
|
||||
class="mt-1 p-2 h-8 flex flex-row items-center hover:bg-surface-hover cursor-pointer rounded-md"
|
||||
>
|
||||
<span class="text-xs min-w-[5rem]">Cancel jobs</span>
|
||||
<ChevronDown class="w-5 h-5" />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</DropdownV2>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 py-1">
|
||||
<div class="relative flex gap-2 items-center pr-8 w-40">
|
||||
@@ -834,10 +1028,11 @@
|
||||
externalJobs={externalJobs ?? []}
|
||||
omittedObscuredJobs={extendedJobs?.omitted_obscured_jobs ?? false}
|
||||
showExternalJobs={!graphIsRunsChart}
|
||||
{isSelectingJobsToCancel}
|
||||
bind:selectedIds
|
||||
bind:selectedWorkspace
|
||||
on:select={() => {
|
||||
runDrawer.openDrawer()
|
||||
if (!isSelectingJobsToCancel) runDrawer.openDrawer()
|
||||
}}
|
||||
on:filterByPath={filterByPath}
|
||||
on:filterByUser={filterByUser}
|
||||
|
||||
Reference in New Issue
Block a user