mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 00:06:14 +00:00
feat(frontend): improve display of waiting jobs + schedule filter + suspended jobs on runs page
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT coalesce(COUNT(*) FILTER(WHERE suspend = 0), 0) as \"database_length!\", coalesce(COUNT(*) FILTER(WHERE suspend > 0), 0) as \"suspended!\" FROM queue WHERE (workspace_id = $1 OR $2) AND scheduled_for <= now() AND running = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "database_length!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "suspended!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "44d0c89afbc472165d80ca7a14b391bc2e5c44e0953103790dfe9375f605ee65"
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "bool",
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE (workspace_id = $1 OR $2) AND scheduled_for <= now() AND running = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "database_length!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "8481d1bd91b7d23f946b4cf0d312acfc0ed4973309b4860e93882a37a21a0bd0"
|
||||
}
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
|
||||
|
||||
+8
-2
@@ -1,12 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM completed_job WHERE workspace_id = $1",
|
||||
"query": "SELECT coalesce(COUNT(*), 0) as \"database_length!\", null::bigint as suspended FROM completed_job WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "database_length!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "suspended",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -15,8 +20,9 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "f5681df5935ec753f4a6d9ab8b927bbf890f0b4fe2de16dc764b24f24d7bbdca"
|
||||
"hash": "bf04a017bc55f6e1f5f25f697b3c4828611591c76dd32541735095c010d6cdf2"
|
||||
}
|
||||
@@ -5631,6 +5631,8 @@ paths:
|
||||
properties:
|
||||
database_length:
|
||||
type: integer
|
||||
suspended:
|
||||
type: integer
|
||||
required:
|
||||
- database_length
|
||||
|
||||
@@ -5812,6 +5814,7 @@ paths:
|
||||
- $ref: "#/components/parameters/CreatedOrStartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedOrStartedAfterCompletedJob"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/Suspended"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/Tag"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
|
||||
@@ -1462,7 +1462,6 @@ async fn cancel_selection(
|
||||
Path(w_id): Path<String>,
|
||||
Json(jobs): Json<Vec<Uuid>>,
|
||||
) -> error::JsonResult<Vec<Uuid>> {
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let jobs_to_cancel = sqlx::query_scalar!(
|
||||
"SELECT id FROM queue WHERE id = ANY($1) AND schedule_path IS NULL",
|
||||
@@ -1508,6 +1507,7 @@ async fn list_filtered_uuids(
|
||||
#[derive(Serialize, Debug, FromRow)]
|
||||
struct QueueStats {
|
||||
database_length: i64,
|
||||
suspended: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -1523,7 +1523,7 @@ async fn count_queue_jobs(
|
||||
Ok(Json(
|
||||
sqlx::query_as!(
|
||||
QueueStats,
|
||||
"SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE (workspace_id = $1 OR $2) AND scheduled_for <= now() AND running = false",
|
||||
"SELECT coalesce(COUNT(*) FILTER(WHERE suspend = 0 AND running = false), 0) as \"database_length!\", coalesce(COUNT(*) FILTER(WHERE suspend > 0), 0) as \"suspended!\" FROM queue WHERE (workspace_id = $1 OR $2) AND scheduled_for <= now()",
|
||||
w_id,
|
||||
w_id == "admins" && cq.all_workspaces.unwrap_or(false),
|
||||
)
|
||||
@@ -1539,7 +1539,7 @@ async fn count_completed_jobs(
|
||||
Ok(Json(
|
||||
sqlx::query_as!(
|
||||
QueueStats,
|
||||
"SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM completed_job WHERE workspace_id = $1",
|
||||
"SELECT coalesce(COUNT(*), 0) as \"database_length!\", null::bigint as suspended FROM completed_job WHERE workspace_id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
export let label: string | null = null
|
||||
export let folder: string | null
|
||||
export let path: string | null
|
||||
export let success: 'success' | 'failure' | 'running' | undefined = undefined
|
||||
export let success: 'success' | 'suspended' | 'waiting' | 'failure' | 'running' | undefined = undefined
|
||||
export let isSkipped: boolean = false
|
||||
export let showSchedules: boolean = true
|
||||
export let showFutureJobs: boolean = true
|
||||
@@ -32,6 +32,8 @@
|
||||
export let maxTs: string | undefined = undefined
|
||||
export let jobKinds: string = ''
|
||||
export let queue_count: Tweened<number> | undefined = undefined
|
||||
export let suspended_count: Tweened<number> | undefined = undefined
|
||||
|
||||
export let autoRefresh: boolean = true
|
||||
export let completedJobs: CompletedJob[] | undefined = undefined
|
||||
export let externalJobs: Job[] | undefined = undefined
|
||||
@@ -63,6 +65,7 @@
|
||||
lookback &&
|
||||
user &&
|
||||
folder &&
|
||||
schedulePath != undefined &&
|
||||
showFutureJobs != undefined &&
|
||||
showSchedules != undefined &&
|
||||
allWorkspaces != undefined &&
|
||||
@@ -137,17 +140,18 @@
|
||||
scriptPathStart: scriptPathStart,
|
||||
jobKinds,
|
||||
success: success == 'success' ? true : success == 'failure' ? false : undefined,
|
||||
running: success == 'running' ? true : undefined,
|
||||
running: (success == 'running' || success == 'suspended' ) ? true : (success == 'waiting' ) ? false : undefined,
|
||||
isSkipped: isSkipped ? undefined : false,
|
||||
// isFlowStep: jobKindsCat != 'all' ? false : undefined,
|
||||
hasNullParent:
|
||||
scriptPathExact != undefined || scriptPathStart != undefined || jobKinds != 'all'
|
||||
scriptPathExact != undefined || scriptPathStart != undefined || jobKindsCat != 'all'
|
||||
? true
|
||||
: undefined,
|
||||
label: label === null || label === '' ? undefined : label,
|
||||
tag: tag === null || tag === '' ? undefined : tag,
|
||||
isNotSchedule: showSchedules == false ? true : undefined,
|
||||
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
|
||||
suspended: success == 'waiting' ? false : success == 'suspended' ? true : undefined,
|
||||
scheduledForBeforeNow: showFutureJobs == false || (success == 'waiting' || success == 'suspended') ? true : undefined,
|
||||
args:
|
||||
argFilter && argFilter != '{}' && argFilter != '' && argError == ''
|
||||
? argFilter
|
||||
@@ -292,16 +296,26 @@
|
||||
}
|
||||
|
||||
async function getCount() {
|
||||
const qc = (await JobService.getQueueCount({ workspace: $workspaceStore!, allWorkspaces }))
|
||||
.database_length
|
||||
const { database_length, suspended} = (await JobService.getQueueCount({ workspace: $workspaceStore!, allWorkspaces }))
|
||||
|
||||
if (queue_count) {
|
||||
queue_count.set(qc)
|
||||
queue_count.set(database_length)
|
||||
} else {
|
||||
queue_count = tweened(qc, { duration: 1000 })
|
||||
queue_count = tweened(database_length, { duration: 1000 })
|
||||
}
|
||||
if (suspended_count) {
|
||||
suspended_count.set(suspended ?? 0)
|
||||
} else {
|
||||
suspended_count = tweened(suspended ?? 0, { duration: 1000 })
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async function syncer() {
|
||||
if (success == 'waiting') {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
}
|
||||
if (loadingFetch) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="text-secondary text-xs">Only showing the first 100 jobs</div>
|
||||
{/if}
|
||||
|
||||
{#each jobs.slice(0, 100) as job}
|
||||
{#each jobs.slice(0, 1000) as job}
|
||||
<div class="flex">
|
||||
<a
|
||||
target="_blank"
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
{job.script_path}
|
||||
</a>
|
||||
<Button
|
||||
title="Filter by path"
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
@@ -163,6 +164,7 @@
|
||||
{/if}
|
||||
{#if job.script_path?.startsWith('f/')}
|
||||
<Button
|
||||
title="Filter by folder"
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
@@ -255,6 +257,15 @@
|
||||
{job.schedule_path}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterBySchedule', job.schedule_path)
|
||||
}}
|
||||
>
|
||||
<ListFilter size={10} />
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
import { AlertCircle, CheckCircle2, ChevronDown, Filter, PlayCircle, X } from 'lucide-svelte'
|
||||
import { AlertCircle, CheckCircle2, ChevronDown, Filter, Hourglass, PlayCircle, X } from 'lucide-svelte'
|
||||
import JsonEditor from '../apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import Label from '../Label.svelte'
|
||||
@@ -19,7 +19,7 @@
|
||||
export let label: string | null = null
|
||||
export let concurrencyKey: string | null = null
|
||||
export let tag: string | null = null
|
||||
export let success: 'running' | 'success' | 'failure' | undefined = undefined
|
||||
export let success: 'running' | 'waiting' | 'suspended' | 'queued' | 'success' | 'failure' | undefined = undefined
|
||||
export let isSkipped: boolean | undefined = undefined
|
||||
export let argFilter: string
|
||||
export let argError: string
|
||||
@@ -29,6 +29,7 @@
|
||||
export let user: string | null = null
|
||||
export let folder: string | null = null
|
||||
export let mobile: boolean = false
|
||||
export let schedulePath: string | undefined
|
||||
|
||||
// Autocomplete data
|
||||
export let paths: string[] = []
|
||||
@@ -39,35 +40,42 @@
|
||||
$: displayedLabel = label
|
||||
$: displayedConcurrencyKey = concurrencyKey
|
||||
$: displayedTag = tag
|
||||
$: displayedSchedule = schedulePath
|
||||
|
||||
let copyArgFilter = argFilter
|
||||
let copyResultFilter = resultFilter
|
||||
|
||||
export let filterBy: 'path' | 'user' | 'folder' | 'label' | 'concurrencyKey' | 'tag' = 'path'
|
||||
export let filterBy: 'path' | 'user' | 'folder' | 'label' | 'concurrencyKey' | 'tag' | 'schedulePath' = 'path'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let manuallySet = false
|
||||
let autoSet = false
|
||||
|
||||
$: {
|
||||
|
||||
$: (path || user || folder || label || concurrencyKey || tag || schedulePath) && autosetFilter()
|
||||
|
||||
function autosetFilter() {
|
||||
if (path !== null && path !== '' && filterBy !== 'path') {
|
||||
manuallySet = true
|
||||
autoSet = true
|
||||
filterBy = 'path'
|
||||
} else if (user !== null && user !== '' && filterBy !== 'user') {
|
||||
manuallySet = true
|
||||
autoSet = true
|
||||
filterBy = 'user'
|
||||
} else if (folder !== null && folder !== '' && filterBy !== 'folder') {
|
||||
manuallySet = true
|
||||
autoSet = true
|
||||
filterBy = 'folder'
|
||||
} else if (label !== null && label !== '' && filterBy !== 'label') {
|
||||
manuallySet = true
|
||||
autoSet = true
|
||||
filterBy = 'label'
|
||||
} else if (concurrencyKey !== null && concurrencyKey !== '' && filterBy !== 'concurrencyKey') {
|
||||
manuallySet = true
|
||||
autoSet = true
|
||||
filterBy = 'concurrencyKey'
|
||||
} else if (tag !== null && tag !== '' && filterBy !== 'tag') {
|
||||
manuallySet = true
|
||||
autoSet = true
|
||||
filterBy = 'tag'
|
||||
} else if (schedulePath !== undefined && schedulePath !== '' && filterBy !== 'schedulePath') {
|
||||
autoSet = true
|
||||
filterBy = 'schedulePath'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +83,8 @@
|
||||
let concurrencyKeyTimeout: NodeJS.Timeout | undefined = undefined
|
||||
let tagTimeout: NodeJS.Timeout | undefined = undefined
|
||||
</script>
|
||||
|
||||
{autoSet}
|
||||
{filterBy}
|
||||
<div class="flex gap-4">
|
||||
{#if !mobile}
|
||||
<div class="flex gap-2">
|
||||
@@ -94,15 +103,16 @@
|
||||
<ToggleButtonGroup
|
||||
bind:selected={filterBy}
|
||||
on:selected={() => {
|
||||
if (!manuallySet) {
|
||||
if (!autoSet) {
|
||||
path = null
|
||||
user = null
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
schedulePath = undefined
|
||||
} else {
|
||||
manuallySet = false
|
||||
autoSet = false
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -111,7 +121,8 @@
|
||||
<ToggleButton value="folder" label="Folder" />
|
||||
<ToggleButtonMore
|
||||
togglableItems={[
|
||||
{ label: 'Concurrency key', value: 'concurrencyKey' },
|
||||
{ label: 'Schedule Path', value: 'schedulePath' },
|
||||
{ label: 'Concurrency Key', value: 'concurrencyKey' },
|
||||
{ label: 'Label', value: 'label' },
|
||||
{ label: 'Tag', value: 'tag' }
|
||||
]}
|
||||
@@ -336,6 +347,39 @@
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{:else if filterBy === 'schedulePath'}
|
||||
{#key tag}
|
||||
<div class="relative">
|
||||
{#if tag}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
schedulePath = undefined
|
||||
dispatch('reset')
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<span class="text-xs absolute -top-4"> Schedule Path </span>
|
||||
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-64"
|
||||
bind:value={displayedSchedule}
|
||||
on:keydown={(e) => {
|
||||
if (tagTimeout) {
|
||||
clearTimeout(tagTimeout)
|
||||
}
|
||||
|
||||
tagTimeout = setTimeout(() => {
|
||||
schedulePath = displayedSchedule
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="relative">
|
||||
@@ -393,6 +437,23 @@
|
||||
icon={AlertCircle}
|
||||
iconProps={{ color: success === 'failure' ? 'red' : 'gray' }}
|
||||
/>
|
||||
{#if success == 'waiting'}
|
||||
<ToggleButton
|
||||
value={'waiting'}
|
||||
tooltip="Waiting"
|
||||
class="whitespace-nowrap"
|
||||
icon={Hourglass}
|
||||
iconProps={{ color: 'blue'}}
|
||||
/>
|
||||
{:else if success == 'suspended'}
|
||||
<ToggleButton
|
||||
value={'suspended'}
|
||||
tooltip="Suspended"
|
||||
class="whitespace-nowrap"
|
||||
icon={Hourglass}
|
||||
iconProps={{ color: 'blue'}}
|
||||
/>
|
||||
{/if}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -419,24 +480,27 @@
|
||||
<ToggleButtonGroup
|
||||
bind:selected={filterBy}
|
||||
on:selected={() => {
|
||||
if (!manuallySet) {
|
||||
if (!autoSet) {
|
||||
path = null
|
||||
user = null
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
schedulePath = undefined
|
||||
} else {
|
||||
manuallySet = false
|
||||
autoSet = false
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="path" label="Path" />
|
||||
<ToggleButton value="user" label="User" />
|
||||
<ToggleButton value="folder" label="Folder" />
|
||||
<ToggleButton value="schedulePath" label="Schedule" />
|
||||
<ToggleButton value="concurrencyKey" label="Concurrency" />
|
||||
<ToggleButton value="tag" label="Tag" />
|
||||
<ToggleButton value="label" label="Label" />
|
||||
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import type { Tweened } from 'svelte/motion'
|
||||
import QueuePopover from './QueuePopover.svelte'
|
||||
import Popup from '../common/popup/Popup.svelte'
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Button } from '../common'
|
||||
import { FilterX, ListFilter } from 'lucide-svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
|
||||
export let queue_count: Tweened<number> | undefined = undefined
|
||||
export let allWorkspaces: boolean = false
|
||||
export let suspended_count: Tweened<number> | undefined = undefined
|
||||
export let success: string | undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="flex gap-1 relative max-w-36 min-w-[50px] items-baseline">
|
||||
<div class="text-xs absolute -top-4 truncate">Jobs waiting for a worker</div>
|
||||
<div class="text-xs absolute -top-4 truncate flex items-baseline gap-1">Waiting for workers <Tooltip small>Jobs waiting for a worker being available to be executed</Tooltip></div>
|
||||
<div class="mt-1">{queue_count ? ($queue_count ?? 0).toFixed(0) : '...'}</div>
|
||||
<div class="truncate text-2xs text-blue-400">
|
||||
<div class="truncate text-2xs !text-secondary mt-0.5">
|
||||
{#if queue_count && ($queue_count ?? 0) > 0}
|
||||
<Popup>
|
||||
<svelte:fragment slot="button">
|
||||
<span class="text-2xs truncate">jobs</span>
|
||||
</svelte:fragment>
|
||||
<QueuePopover {allWorkspaces} />
|
||||
</Popup>
|
||||
<Button size="xs2" color="light" on:click={() => dispatch('jobs_waiting')}>{#if success == 'waiting'}<FilterX size={12}></FilterX>{:else}<ListFilter size={12}></ListFilter>{/if}</Button>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if suspended_count && ($suspended_count ?? 0) > 0}
|
||||
<div class="flex gap-1 relative max-w-36 min-w-[50px] items-baseline ml-20 mr-8">
|
||||
<div class="text-xs absolute -top-4 truncate flex items-baseline gap-1">Suspended <Tooltip small>Jobs waiting for an event or approval before being resumed</Tooltip></div>
|
||||
<div class="mt-1">{suspended_count ? ($suspended_count ?? 0).toFixed(0) : '...'}</div>
|
||||
<div class="truncate text-2xs !text-secondary mt-0.5">
|
||||
<Button size="xs2" color="light" on:click={() => dispatch('jobs_suspended')}>{#if success == 'waiting'}<FilterX size={12}></FilterX>{:else}<ListFilter size={12}></ListFilter>{/if}</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -228,73 +228,74 @@
|
||||
<div class="w-3/12 text-xs font-semibold">Triggered by</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if jobs?.length == 0 && (!showExternalJobs || externalJobs?.length == 0)}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center p-8">
|
||||
<div class="text-xs text-secondary"> No jobs found for the selected filters. </div>
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<VirtualList
|
||||
width="100%"
|
||||
height={tableHeight - header}
|
||||
itemCount={flatJobs?.length ?? 3}
|
||||
itemSize={42}
|
||||
{stickyIndices}
|
||||
>
|
||||
<div slot="item" let:index let:style {style} class="w-full">
|
||||
{#if flatJobs}
|
||||
{@const jobOrDate = flatJobs[index]}
|
||||
|
||||
<VirtualList
|
||||
width="100%"
|
||||
height={tableHeight - header}
|
||||
itemCount={flatJobs?.length ?? 3}
|
||||
itemSize={42}
|
||||
{stickyIndices}
|
||||
>
|
||||
<div slot="item" let:index let:style {style} class="w-full">
|
||||
{#if flatJobs}
|
||||
{@const jobOrDate = flatJobs[index]}
|
||||
|
||||
{#if jobOrDate}
|
||||
{#if jobOrDate?.type === 'date'}
|
||||
<div class="bg-surface-secondary py-2 border-b font-semibold text-xs pl-5">
|
||||
{jobOrDate.date}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row items-center h-full w-full">
|
||||
<RunRow
|
||||
{containsLabel}
|
||||
job={jobOrDate.job}
|
||||
selected={jobOrDate.job.id !== '-' && selectedIds.includes(jobOrDate.job.id)}
|
||||
{isSelectingJobsToCancel}
|
||||
on:select={() => {
|
||||
const jobId = jobOrDate.job.id
|
||||
if (isSelectingJobsToCancel) {
|
||||
if (selectedIds.includes(jobOrDate.job.id)) {
|
||||
selectedIds = selectedIds.filter((id) => id != jobId)
|
||||
{#if jobOrDate}
|
||||
{#if jobOrDate?.type === 'date'}
|
||||
<div class="bg-surface-secondary py-2 border-b font-semibold text-xs pl-5">
|
||||
{jobOrDate.date}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row items-center h-full w-full">
|
||||
<RunRow
|
||||
{containsLabel}
|
||||
job={jobOrDate.job}
|
||||
selected={jobOrDate.job.id !== '-' && selectedIds.includes(jobOrDate.job.id)}
|
||||
{isSelectingJobsToCancel}
|
||||
on: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 {
|
||||
selectedIds.push(jobId)
|
||||
selectedIds = selectedIds
|
||||
selectedWorkspace = jobOrDate.job.workspace_id
|
||||
selectedIds = [jobOrDate.job.id]
|
||||
dispatch('select')
|
||||
}
|
||||
} else {
|
||||
selectedWorkspace = jobOrDate.job.workspace_id
|
||||
selectedIds = [jobOrDate.job.id]
|
||||
dispatch('select')
|
||||
}
|
||||
}}
|
||||
{activeLabel}
|
||||
on:filterByLabel
|
||||
on:filterByPath
|
||||
on:filterByUser
|
||||
on:filterByFolder
|
||||
on:filterByConcurrencyKey
|
||||
{containerWidth}
|
||||
/>
|
||||
</div>
|
||||
}}
|
||||
{activeLabel}
|
||||
on:filterByLabel
|
||||
on:filterByPath
|
||||
on:filterByUser
|
||||
on:filterByFolder
|
||||
on:filterByConcurrencyKey
|
||||
on:filterBySchedule
|
||||
{containerWidth}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{JSON.stringify(jobOrDate)}
|
||||
{/if}
|
||||
{:else}
|
||||
{JSON.stringify(jobOrDate)}
|
||||
<div class="flex flex-row items-center h-full w-full">
|
||||
<div class="w-1/12 text-2xs">...</div>
|
||||
<div class="w-4/12 text-xs">...</div>
|
||||
<div class="w-4/12 text-xs">...</div>
|
||||
<div class="w-3/12 text-xs">...</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-row items-center h-full w-full">
|
||||
<div class="w-1/12 text-2xs">...</div>
|
||||
<div class="w-4/12 text-xs">...</div>
|
||||
<div class="w-4/12 text-xs">...</div>
|
||||
<div class="w-3/12 text-xs">...</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</VirtualList>
|
||||
</div>
|
||||
</VirtualList>
|
||||
{/if}
|
||||
</div>
|
||||
{#if jobs?.length == 0 && (!showExternalJobs || externalJobs?.length == 0)}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-8">
|
||||
<div class="text-xs text-secondary"> No jobs found for the selected filters. </div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
let concurrencyKey: string | null = $page.url.searchParams.get('concurrency_key')
|
||||
let tag: string | null = $page.url.searchParams.get('tag')
|
||||
// Rest of filters handled by RunsFilter
|
||||
let success: 'running' | 'success' | 'failure' | undefined = ($page.url.searchParams.get(
|
||||
let success: 'running' | 'suspended' | 'waiting' | 'success' | 'failure' | undefined = ($page.url.searchParams.get(
|
||||
'success'
|
||||
) ?? undefined) as 'running' | 'success' | 'failure' | undefined
|
||||
let isSkipped: boolean | undefined =
|
||||
@@ -87,6 +87,8 @@
|
||||
let allWorkspaces = $page.url.searchParams.get('all_workspaces') == 'true' ?? false
|
||||
|
||||
let queue_count: Tweened<number> | undefined = undefined
|
||||
let suspended_count: Tweened<number> | undefined = undefined
|
||||
|
||||
let jobKinds: string | undefined = undefined
|
||||
let loading: boolean = false
|
||||
let paths: string[] = []
|
||||
@@ -255,8 +257,6 @@
|
||||
function reset() {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
|
||||
autoRefresh = true
|
||||
jobs = undefined
|
||||
completedJobs = undefined
|
||||
selectedManualDate = 0
|
||||
@@ -296,6 +296,8 @@
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
schedulePath = undefined
|
||||
|
||||
}
|
||||
|
||||
function filterByUser(e: CustomEvent<string>) {
|
||||
@@ -305,6 +307,7 @@
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
schedulePath = undefined
|
||||
}
|
||||
|
||||
function filterByFolder(e: CustomEvent<string>) {
|
||||
@@ -314,6 +317,7 @@
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
schedulePath = undefined
|
||||
}
|
||||
|
||||
function filterByLabel(e: CustomEvent<string>) {
|
||||
@@ -323,6 +327,7 @@
|
||||
label = e.detail
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
schedulePath = undefined
|
||||
}
|
||||
|
||||
function filterByConcurrencyKey(e: CustomEvent<string>) {
|
||||
@@ -332,6 +337,7 @@
|
||||
label = null
|
||||
concurrencyKey = e.detail
|
||||
tag = null
|
||||
schedulePath = undefined
|
||||
}
|
||||
|
||||
function filterByTag(e: CustomEvent<string>) {
|
||||
@@ -341,6 +347,17 @@
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = e.detail
|
||||
schedulePath = undefined
|
||||
}
|
||||
|
||||
function filterBySchedule(e: CustomEvent<string>) {
|
||||
path = null
|
||||
user = null
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
schedulePath = e.detail
|
||||
}
|
||||
|
||||
let calendarChangeTimeout: NodeJS.Timeout | undefined = undefined
|
||||
@@ -381,18 +398,27 @@
|
||||
scriptPathStart: folder === null || folder === '' ? undefined : `f/${folder}/`,
|
||||
jobKinds,
|
||||
success: success == 'success' ? true : success == 'failure' ? false : undefined,
|
||||
running: success == 'running' ? true : undefined,
|
||||
running: (success == 'running' || success == 'suspended' ) ? true : (success == 'waiting' ) ? false : undefined,
|
||||
isSkipped: isSkipped ? undefined : false,
|
||||
// isFlowStep: jobKindsCat != 'all' ? false : undefined,
|
||||
hasNullParent:
|
||||
path != undefined || path != undefined || jobKindsCat != 'all'
|
||||
? true
|
||||
: undefined,
|
||||
label: label === null || label === '' ? undefined : label,
|
||||
tag: tag === null || tag === '' ? undefined : tag,
|
||||
isNotSchedule: showSchedules == false ? true : undefined,
|
||||
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
|
||||
suspended: success == 'waiting' ? false : success == 'suspended' ? true : undefined,
|
||||
scheduledForBeforeNow: showFutureJobs == false || (success == 'waiting' || success == 'suspended') ? true : undefined,
|
||||
args:
|
||||
argFilter && argFilter != '{}' && argFilter != '' && argError == '' ? argFilter : undefined,
|
||||
argFilter && argFilter != '{}' && argFilter != '' && argError == ''
|
||||
? argFilter
|
||||
: undefined,
|
||||
result:
|
||||
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
|
||||
? resultFilter
|
||||
: undefined,
|
||||
allWorkspaces: allWorkspaces ? true : undefined,
|
||||
concurrencyKey: concurrencyKey ?? undefined,
|
||||
tag: tag ?? undefined
|
||||
}
|
||||
|
||||
selectedFiltersString = JSON.stringify(selectedFilters, null, 4)
|
||||
@@ -424,6 +450,24 @@
|
||||
graph === 'ConcurrencyChart' &&
|
||||
extendedJobs !== undefined &&
|
||||
extendedJobs.jobs.length + extendedJobs.obscured_jobs.length >= 1000
|
||||
|
||||
|
||||
function jobsFilter(f: 'waiting' | 'suspended') {
|
||||
path = null
|
||||
user = null
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
schedulePath = undefined
|
||||
path = null
|
||||
tag = null
|
||||
if (success == f) {
|
||||
success = undefined
|
||||
} else {
|
||||
success = f
|
||||
}
|
||||
jobKindsCat = 'all'
|
||||
}
|
||||
</script>
|
||||
|
||||
<JobLoader
|
||||
@@ -446,6 +490,7 @@
|
||||
bind:maxTs
|
||||
{jobKinds}
|
||||
bind:queue_count
|
||||
bind:suspended_count
|
||||
{autoRefresh}
|
||||
bind:completedJobs
|
||||
bind:externalJobs
|
||||
@@ -518,7 +563,7 @@
|
||||
|
||||
<svelte:window bind:innerWidth />
|
||||
|
||||
{#if innerWidth > 1280}
|
||||
{#if innerWidth > 900}
|
||||
<div class="w-full h-screen">
|
||||
<div class="px-2">
|
||||
<div class="flex items-center space-x-2 flex-row justify-between">
|
||||
@@ -556,6 +601,7 @@
|
||||
bind:resultError
|
||||
bind:jobKindsCat
|
||||
bind:allWorkspaces
|
||||
bind:schedulePath
|
||||
on:change={reloadJobsWithoutFilterError}
|
||||
{usernames}
|
||||
{folders}
|
||||
@@ -651,7 +697,11 @@
|
||||
</div>
|
||||
<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} />
|
||||
<RunsQueue {success} {queue_count} {suspended_count} on:jobs_waiting={() => {
|
||||
jobsFilter('waiting')
|
||||
}} on:jobs_suspended={() => {
|
||||
jobsFilter('suspended')
|
||||
}} />
|
||||
<div class="flex flex-row">
|
||||
{#if isSelectingJobsToCancel}
|
||||
<div class="mt-1 p-2 h-8 flex flex-row items-center gap-1">
|
||||
@@ -724,7 +774,7 @@
|
||||
localStorage.setItem('show_schedules_in_run', showSchedules ? 'true' : 'false')
|
||||
}}
|
||||
/>
|
||||
<span class="text-xs absolute -top-4">CRON Schedules</span>
|
||||
<span class="text-xs absolute -top-4"><span class="hidden xl:inline">CRON</span> Schedules</span>
|
||||
|
||||
<Calendar size={16} />
|
||||
</div>
|
||||
@@ -843,6 +893,7 @@
|
||||
on:filterByLabel={filterByLabel}
|
||||
on:filterByConcurrencyKey={filterByConcurrencyKey}
|
||||
on:filterByTag={filterByTag}
|
||||
on:filterBySchedule={filterBySchedule}
|
||||
/>
|
||||
{:else}
|
||||
<div class="gap-1 flex flex-col">
|
||||
@@ -909,6 +960,7 @@
|
||||
bind:argError
|
||||
bind:resultError
|
||||
bind:allWorkspaces
|
||||
bind:schedulePath
|
||||
mobile={true}
|
||||
on:change={reloadJobsWithoutFilterError}
|
||||
/>
|
||||
@@ -992,10 +1044,14 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 md:flex-row w-full p-4">
|
||||
<div class="flex flex-col gap-4 md:flex-row w-full p-4 overflow-x-auto">
|
||||
<div class="flex items-center flex-row gap-2 grow">
|
||||
{#if queue_count}
|
||||
<RunsQueue {queue_count} {allWorkspaces} />
|
||||
<RunsQueue {success} {queue_count} {suspended_count} on:jobs_waiting={() => {
|
||||
jobsFilter('waiting')
|
||||
}} on:jobs_suspended={() => {
|
||||
jobsFilter('suspended')
|
||||
}} />
|
||||
{/if}
|
||||
<div class="flex flex-row">
|
||||
{#if isSelectingJobsToCancel}
|
||||
@@ -1062,7 +1118,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 py-1">
|
||||
<div class="relative flex gap-2 items-center pr-8 w-40">
|
||||
<div class="relative flex gap-2 items-center pr-8 w-20">
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={showSchedules}
|
||||
@@ -1074,7 +1130,7 @@
|
||||
|
||||
<Calendar size={16} />
|
||||
</div>
|
||||
<div class="relative flex gap-2 items-center pr-8 w-40">
|
||||
<div class="relative flex gap-2 items-center pr-8 w-20">
|
||||
<span class="text-xs absolute -top-4">Planned later</span>
|
||||
<Toggle
|
||||
size="xs"
|
||||
@@ -1093,6 +1149,7 @@
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="min-w-10"
|
||||
value={minTs
|
||||
? new Date(minTs).toLocaleString()
|
||||
: 'zoom x axis to set min (drag with ctrl)'}
|
||||
@@ -1124,6 +1181,7 @@
|
||||
<div class="flex gap-1 relative w-full">
|
||||
<span class="text-xs absolute -top-4">Max datetime</span>
|
||||
<input
|
||||
class="min-w-10"
|
||||
type="text"
|
||||
value={maxTs ? new Date(maxTs).toLocaleString() : 'zoom x axis to set max'}
|
||||
disabled
|
||||
|
||||
@@ -373,7 +373,7 @@
|
||||
/>
|
||||
<div class="flex gap-2 items-center justify-end">
|
||||
<Button
|
||||
href={`${base}/runs/?schedule_path=${path}`}
|
||||
href={`${base}/runs/?schedule_path=${path}&show_schedules=true&show_future_jobs=true`}
|
||||
size="xs"
|
||||
startIcon={{ icon: List }}
|
||||
color="light"
|
||||
|
||||
Reference in New Issue
Block a user