mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 00:02:03 +00:00
feat: improve runs page + add all workspaces to admins runs page
This commit is contained in:
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE workspace_id = $1 AND scheduled_for <= now() AND running = false",
|
||||
"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": [
|
||||
{
|
||||
@@ -11,12 +11,13 @@
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "9da1bf41c433c1f781bcc8ea5d86fd796b7ff92434f5ed4682c4445a02c2c720"
|
||||
"hash": "8481d1bd91b7d23f946b4cf0d312acfc0ed4973309b4860e93882a37a21a0bd0"
|
||||
}
|
||||
@@ -4814,6 +4814,11 @@ paths:
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- $ref: "#/components/parameters/Tag"
|
||||
- name: all_workspaces
|
||||
description: get jobs from all workspaces (only valid if request come from the `admins` workspace)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: All queued jobs
|
||||
@@ -4832,6 +4837,11 @@ paths:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: all_workspaces
|
||||
description: get jobs from all workspaces (only valid if request come from the `admins` workspace)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: queue count
|
||||
@@ -4965,6 +4975,11 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: all_workspaces
|
||||
description: get jobs from all workspaces (only valid if request come from the `admins` workspace)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: All jobs
|
||||
|
||||
@@ -82,7 +82,7 @@ pub struct ListableApp {
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub execution_mode: String,
|
||||
pub starred: bool,
|
||||
pub edited_at: chrono::DateTime<chrono::Utc>,
|
||||
pub edited_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub has_draft: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub draft_only: Option<bool>,
|
||||
|
||||
@@ -663,6 +663,7 @@ pub struct ListQueueQuery {
|
||||
pub args: Option<String>,
|
||||
pub tag: Option<String>,
|
||||
pub scheduled_for_before_now: Option<bool>,
|
||||
pub all_workspaces: Option<bool>,
|
||||
}
|
||||
|
||||
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
|
||||
@@ -670,9 +671,12 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
||||
.fields(fields)
|
||||
.order_by("created_at", lq.order_desc.unwrap_or(true))
|
||||
.limit(1000)
|
||||
.and_where_eq("workspace_id", "?".bind(&w_id))
|
||||
.clone();
|
||||
|
||||
if w_id != "admins" || !lq.all_workspaces.is_some_and(|x| x) {
|
||||
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
|
||||
}
|
||||
|
||||
if let Some(ps) = &lq.script_path_start {
|
||||
sqlb.and_where_like_left("script_path", "?".bind(ps));
|
||||
}
|
||||
@@ -765,6 +769,7 @@ struct ListableQueuedJob {
|
||||
pub suspend: Option<i32>,
|
||||
pub tag: String,
|
||||
pub priority: Option<i16>,
|
||||
pub workspace_id: String,
|
||||
}
|
||||
|
||||
async fn list_queue_jobs(
|
||||
@@ -794,6 +799,7 @@ async fn list_queue_jobs(
|
||||
"suspend",
|
||||
"tag",
|
||||
"priority",
|
||||
"workspace_id",
|
||||
],
|
||||
)
|
||||
.sql()?;
|
||||
@@ -857,15 +863,22 @@ struct QueueStats {
|
||||
database_length: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CountQueueJobsQuery {
|
||||
all_workspaces: Option<bool>,
|
||||
}
|
||||
|
||||
async fn count_queue_jobs(
|
||||
Extension(db): Extension<DB>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(cq): Query<CountQueueJobsQuery>,
|
||||
) -> error::JsonResult<QueueStats> {
|
||||
Ok(Json(
|
||||
sqlx::query_as!(
|
||||
QueueStats,
|
||||
"SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE workspace_id = $1 AND scheduled_for <= now() AND running = false",
|
||||
w_id
|
||||
"SELECT coalesce(COUNT(*), 0) as \"database_length!\" FROM queue WHERE (workspace_id = $1 OR $2) AND scheduled_for <= now() AND running = false",
|
||||
w_id,
|
||||
w_id == "admins" && cq.all_workspaces.unwrap_or(false),
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?,
|
||||
@@ -972,6 +985,7 @@ async fn list_jobs(
|
||||
tag: lq.tag,
|
||||
schedule_path: lq.schedule_path,
|
||||
scheduled_for_before_now: lq.scheduled_for_before_now,
|
||||
all_workspaces: lq.all_workspaces,
|
||||
},
|
||||
&[
|
||||
"'QueuedJob' as typ",
|
||||
@@ -3135,11 +3149,14 @@ fn list_completed_jobs_query(
|
||||
let mut sqlb = SqlBuilder::select_from("completed_job")
|
||||
.fields(fields)
|
||||
.order_by("created_at", lq.order_desc.unwrap_or(true))
|
||||
.and_where_eq("workspace_id", "?".bind(&w_id))
|
||||
.offset(offset)
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
|
||||
if w_id != "admins" || !lq.all_workspaces.is_some_and(|x| x) {
|
||||
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
|
||||
}
|
||||
|
||||
if let Some(p) = &lq.schedule_path {
|
||||
sqlb.and_where_eq("schedule_path", "?".bind(p));
|
||||
}
|
||||
@@ -3229,6 +3246,7 @@ pub struct ListCompletedQuery {
|
||||
pub result: Option<String>,
|
||||
pub tag: Option<String>,
|
||||
pub scheduled_for_before_now: Option<bool>,
|
||||
pub all_workspaces: Option<bool>,
|
||||
}
|
||||
|
||||
async fn list_completed_jobs(
|
||||
|
||||
+1
-1
@@ -528,7 +528,7 @@ export async function ignoreF(wmillconf: {
|
||||
.join(", ");
|
||||
log.info(
|
||||
colors.gray(
|
||||
`(Deprecated, use wmill.conf/includes instead) Using .wmillignore file (${condensed})`
|
||||
`(Deprecated, use wmill.yaml/includes instead) Using .wmillignore file (${condensed})`
|
||||
)
|
||||
);
|
||||
ign = gitignore_parser.compile(ignoreContent);
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
export let jobs: CompletedJob[] | undefined = []
|
||||
export let maxIsNow: boolean = false
|
||||
export let minTimeSet: string | undefined = undefined
|
||||
export let maxTimeSet: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -100,25 +102,56 @@
|
||||
let minTime = addSeconds(new Date(), -300)
|
||||
let maxTime = getDbClockNow()
|
||||
|
||||
$: computeMinMaxTime(jobs)
|
||||
$: computeMinMaxTime(jobs, minTimeSet, maxTimeSet)
|
||||
|
||||
function computeMinMaxTime(jobs: CompletedJob[] | undefined) {
|
||||
if (jobs == undefined || jobs?.length == 0) {
|
||||
minTime = addSeconds(new Date(), -300)
|
||||
maxTime = getDbClockNow()
|
||||
function minJobTime(jobs: CompletedJob[]): Date {
|
||||
let min: Date = new Date(jobs[0].started_at)
|
||||
for (const job of jobs) {
|
||||
if (new Date(job.started_at) < min) {
|
||||
min = new Date(job.started_at)
|
||||
}
|
||||
}
|
||||
return min
|
||||
}
|
||||
|
||||
function maxJobTime(jobs: CompletedJob[]): Date {
|
||||
let max: Date = new Date(jobs[0].started_at)
|
||||
for (const job of jobs) {
|
||||
if (new Date(job.started_at) > max) {
|
||||
max = new Date(job.started_at)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
function computeMinMaxTime(
|
||||
jobs: CompletedJob[] | undefined,
|
||||
minTimeSet: string | undefined,
|
||||
maxTimeSet: string | undefined
|
||||
) {
|
||||
let minTimeSetDate = minTimeSet ? new Date(minTimeSet) : undefined
|
||||
let maxTimeSetDate = maxTimeSet ? new Date(maxTimeSet) : undefined
|
||||
if (minTimeSetDate && maxTimeSetDate) {
|
||||
minTime = minTimeSetDate
|
||||
maxTime = maxTimeSetDate
|
||||
return
|
||||
}
|
||||
|
||||
const maxJob = maxIsNow ? getDbClockNow() : new Date(jobs?.[0].started_at)
|
||||
const minJob = new Date(jobs?.[jobs?.length - 1].started_at)
|
||||
if (jobs == undefined || jobs?.length == 0) {
|
||||
minTime = minTimeSetDate ?? addSeconds(new Date(), -300)
|
||||
maxTime = maxTimeSetDate ?? getDbClockNow()
|
||||
return
|
||||
}
|
||||
|
||||
const maxJob = maxIsNow ? getDbClockNow() : maxJobTime(jobs)
|
||||
const minJob = minJobTime(jobs)
|
||||
|
||||
const diff = (maxJob.getTime() - minJob.getTime()) / 20000
|
||||
|
||||
minTime = addSeconds(minJob, -diff)
|
||||
minTime = minTimeSetDate ?? addSeconds(minJob, -diff)
|
||||
if (maxIsNow) {
|
||||
maxTime = maxJob
|
||||
maxTime = maxTimeSetDate ?? maxJob
|
||||
} else {
|
||||
maxTime = addSeconds(maxJob, diff)
|
||||
maxTime = maxTimeSetDate ?? addSeconds(maxJob, diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +201,13 @@
|
||||
} as any
|
||||
</script>
|
||||
|
||||
<!-- {JSON.stringify(minTime)}
|
||||
{JSON.stringify(maxTime)}
|
||||
|
||||
{JSON.stringify(jobs?.map((x) => x.started_at))} -->
|
||||
<!-- {minTime}
|
||||
{maxTime} -->
|
||||
<!-- {JSON.stringify(jobs?.map((x) => x.started_at))} -->
|
||||
<div class="relative max-h-40">
|
||||
<Scatter {data} options={scatterOptions} />
|
||||
</div>
|
||||
|
||||
@@ -153,9 +153,9 @@
|
||||
success="running"
|
||||
argFilter={undefined}
|
||||
bind:loading
|
||||
synUrl={false}
|
||||
syncQueuedRunsCount={false}
|
||||
refreshRate={10000}
|
||||
computeMinAndMax={undefined}
|
||||
/>
|
||||
|
||||
<div class="min-w-[300px] h-full">
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
errorIteration += 1
|
||||
if (errorIteration == 5) {
|
||||
notfound = true
|
||||
await clearCurrentJob()
|
||||
job = undefined
|
||||
}
|
||||
console.warn(err)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import {
|
||||
JobService,
|
||||
Job,
|
||||
CompletedJob,
|
||||
ScriptService,
|
||||
FlowService,
|
||||
UserService,
|
||||
FolderService
|
||||
} from '$lib/gen'
|
||||
import { JobService, Job, CompletedJob } from '$lib/gen'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
import { tweened, type Tweened } from 'svelte/motion'
|
||||
import { goto } from '$app/navigation'
|
||||
import { forLater } from '$lib/forLater'
|
||||
|
||||
export let jobs: Job[] | undefined
|
||||
@@ -34,65 +24,35 @@
|
||||
export let jobKinds: string = ''
|
||||
export let queue_count: Tweened<number> | undefined = undefined
|
||||
export let autoRefresh: boolean = true
|
||||
export let paths: string[] = []
|
||||
export let usernames: string[] = []
|
||||
export let folders: string[] = []
|
||||
|
||||
export let completedJobs: CompletedJob[] | undefined = undefined
|
||||
export let argError = ''
|
||||
export let resultError = ''
|
||||
export let loading: boolean = false
|
||||
export let synUrl: boolean = true
|
||||
export let refreshRate = 5000
|
||||
export let syncQueuedRunsCount: boolean = true
|
||||
export let allWorkspaces: boolean = false
|
||||
export let computeMinAndMax: (() => { minTs: string; maxTs: string } | undefined) | undefined
|
||||
|
||||
let mounted: boolean = false
|
||||
let intervalId: NodeJS.Timeout | undefined
|
||||
let sync = true
|
||||
|
||||
// This reactive statement is used to sync the url with the current state of the filters
|
||||
$: if (synUrl) {
|
||||
let searchParams = new URLSearchParams()
|
||||
|
||||
user && searchParams.set('user', user)
|
||||
folder && searchParams.set('folder', folder)
|
||||
|
||||
if (success !== undefined) {
|
||||
searchParams.set('success', success.toString())
|
||||
}
|
||||
|
||||
if (isSkipped) {
|
||||
searchParams.set('is_skipped', isSkipped.toString())
|
||||
}
|
||||
|
||||
if (hideSchedules) {
|
||||
searchParams.set('hide_scheduled', hideSchedules.toString())
|
||||
}
|
||||
|
||||
// ArgFilter is an object. Encode it to a string
|
||||
argFilter && searchParams.set('arg', encodeURIComponent(JSON.stringify(argFilter)))
|
||||
resultFilter && searchParams.set('result', encodeURIComponent(JSON.stringify(resultFilter)))
|
||||
schedulePath && searchParams.set('schedule_path', schedulePath)
|
||||
|
||||
jobKindsCat != 'runs' && searchParams.set('job_kinds', jobKindsCat)
|
||||
|
||||
minTs && searchParams.set('min_ts', minTs)
|
||||
maxTs && searchParams.set('max_ts', maxTs)
|
||||
|
||||
let newPath = path ? `/${path}` : '/'
|
||||
let newUrl = `/runs${newPath}?${searchParams.toString()}`
|
||||
|
||||
goto(newUrl)
|
||||
}
|
||||
|
||||
$: jobKinds = computeJobKinds(jobKindsCat)
|
||||
$: ($workspaceStore && loadJobs()) ||
|
||||
(path && success && isSkipped && jobKinds && user && folder && minTs && maxTs && hideSchedules)
|
||||
$: ($workspaceStore && loadJobsIntern(true)) ||
|
||||
(path &&
|
||||
success &&
|
||||
isSkipped != undefined &&
|
||||
jobKinds &&
|
||||
user &&
|
||||
folder &&
|
||||
hideSchedules != undefined &&
|
||||
allWorkspaces != undefined)
|
||||
|
||||
$: if (mounted && !intervalId && autoRefresh) {
|
||||
$: if (!intervalId && autoRefresh) {
|
||||
intervalId = setInterval(syncer, refreshRate)
|
||||
}
|
||||
|
||||
$: if (mounted && intervalId && !autoRefresh) {
|
||||
$: if (intervalId && !autoRefresh) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = undefined
|
||||
}
|
||||
@@ -126,26 +86,41 @@
|
||||
jobKinds,
|
||||
success: success == 'success' ? true : success == 'failure' ? false : undefined,
|
||||
running: success == 'running' ? true : undefined,
|
||||
isSkipped,
|
||||
isSkipped: isSkipped ? true : undefined,
|
||||
isFlowStep: jobKindsCat != 'all' ? false : undefined,
|
||||
args:
|
||||
argFilter && argFilter != '{}' && argFilter != '' && argError == '' ? argFilter : undefined,
|
||||
result:
|
||||
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
|
||||
? resultFilter
|
||||
: undefined
|
||||
: undefined,
|
||||
allWorkspaces: allWorkspaces ? true : undefined
|
||||
})
|
||||
}
|
||||
|
||||
export async function loadJobs(shouldGetCount?: boolean): Promise<void> {
|
||||
export async function loadJobs(
|
||||
nMinTs: string | undefined,
|
||||
nMaxTs: string | undefined,
|
||||
reset: boolean,
|
||||
shouldGetCount?: boolean
|
||||
): Promise<void> {
|
||||
minTs = nMinTs
|
||||
maxTs = nMaxTs
|
||||
if (reset) {
|
||||
jobs = undefined
|
||||
completedJobs = undefined
|
||||
intervalId && clearInterval(intervalId)
|
||||
intervalId = setInterval(syncer, refreshRate)
|
||||
}
|
||||
await loadJobsIntern(shouldGetCount)
|
||||
}
|
||||
async function loadJobsIntern(shouldGetCount?: boolean): Promise<void> {
|
||||
if (shouldGetCount) {
|
||||
getCount()
|
||||
}
|
||||
|
||||
loading = true
|
||||
try {
|
||||
jobs = await fetchJobs(maxTs, minTs)
|
||||
|
||||
computeCompletedJobs()
|
||||
|
||||
if (hideSchedules && !schedulePath) {
|
||||
@@ -161,7 +136,8 @@
|
||||
}
|
||||
|
||||
async function getCount() {
|
||||
const qc = (await JobService.getQueueCount({ workspace: $workspaceStore! })).database_length
|
||||
const qc = (await JobService.getQueueCount({ workspace: $workspaceStore!, allWorkspaces }))
|
||||
.database_length
|
||||
if (queue_count) {
|
||||
queue_count.set(qc)
|
||||
} else {
|
||||
@@ -170,106 +146,69 @@
|
||||
}
|
||||
|
||||
async function syncer() {
|
||||
if (syncQueuedRunsCount) {
|
||||
getCount()
|
||||
}
|
||||
if (sync) {
|
||||
if (syncQueuedRunsCount) {
|
||||
getCount()
|
||||
}
|
||||
|
||||
if (sync && jobs && maxTs == undefined) {
|
||||
if (success == 'running') {
|
||||
loadJobs()
|
||||
} else {
|
||||
let ts: string | undefined = undefined
|
||||
let cursor = 0
|
||||
if (computeMinAndMax) {
|
||||
const ts = computeMinAndMax()
|
||||
if (ts) {
|
||||
minTs = ts.minTs
|
||||
maxTs = ts.maxTs
|
||||
if (maxTs != undefined) {
|
||||
loadJobsIntern(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (cursor < jobs.length && minTs == undefined) {
|
||||
let invCursor = jobs.length - 1 - cursor
|
||||
let isQueuedJob =
|
||||
cursor == jobs?.length - 1 || jobs[invCursor].type == Job.type.QUEUED_JOB
|
||||
if (isQueuedJob) {
|
||||
if (cursor > 0) {
|
||||
const date = new Date(jobs[invCursor + 1]?.created_at!)
|
||||
date.setMilliseconds(date.getMilliseconds() + 1)
|
||||
ts = date.toISOString()
|
||||
if (jobs && maxTs == undefined) {
|
||||
if (success == 'running') {
|
||||
loadJobsIntern(false)
|
||||
} else {
|
||||
let ts: string | undefined = undefined
|
||||
let cursor = 0
|
||||
|
||||
while (cursor < jobs.length && minTs == undefined) {
|
||||
let invCursor = jobs.length - 1 - cursor
|
||||
let isQueuedJob =
|
||||
cursor == jobs?.length - 1 || jobs[invCursor].type == Job.type.QUEUED_JOB
|
||||
if (isQueuedJob) {
|
||||
if (cursor > 0) {
|
||||
const date = new Date(jobs[invCursor + 1]?.created_at!)
|
||||
date.setMilliseconds(date.getMilliseconds() + 1)
|
||||
ts = date.toISOString()
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
cursor++
|
||||
}
|
||||
cursor++
|
||||
}
|
||||
|
||||
loading = true
|
||||
loading = true
|
||||
const newJobs = await fetchJobs(maxTs, minTs ?? ts)
|
||||
if (newJobs && newJobs.length > 0 && jobs) {
|
||||
const oldJobs = jobs?.map((x) => x.id)
|
||||
jobs = newJobs.filter((x) => !oldJobs.includes(x.id)).concat(jobs)
|
||||
newJobs
|
||||
.filter((x) => oldJobs.includes(x.id))
|
||||
.forEach((x) => (jobs![jobs?.findIndex((y) => y.id == x.id)!] = x))
|
||||
jobs = jobs
|
||||
computeCompletedJobs()
|
||||
|
||||
const newJobs = await fetchJobs(maxTs, minTs ?? ts)
|
||||
if (newJobs && newJobs.length > 0 && jobs) {
|
||||
const oldJobs = jobs?.map((x) => x.id)
|
||||
jobs = newJobs.filter((x) => !oldJobs.includes(x.id)).concat(jobs)
|
||||
newJobs
|
||||
.filter((x) => oldJobs.includes(x.id))
|
||||
.forEach((x) => (jobs![jobs?.findIndex((y) => y.id == x.id)!] = x))
|
||||
jobs = jobs
|
||||
computeCompletedJobs()
|
||||
|
||||
if (hideSchedules && !schedulePath) {
|
||||
jobs = jobs.filter(
|
||||
(job) =>
|
||||
!(job && 'running' in job && job.scheduled_for && forLater(job.scheduled_for))
|
||||
)
|
||||
if (hideSchedules && !schedulePath) {
|
||||
jobs = jobs.filter(
|
||||
(job) =>
|
||||
!(job && 'running' in job && job.scheduled_for && forLater(job.scheduled_for))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading = false
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFiltersFromURL() {
|
||||
path = $page.params.path
|
||||
user = $page.url.searchParams.get('user')
|
||||
folder = $page.url.searchParams.get('folder')
|
||||
success = ($page.url.searchParams.get('success') ?? undefined) as
|
||||
| 'success'
|
||||
| 'failure'
|
||||
| 'running'
|
||||
| undefined
|
||||
isSkipped =
|
||||
$page.url.searchParams.get('is_skipped') != undefined
|
||||
? $page.url.searchParams.get('is_skipped') == 'true'
|
||||
: false
|
||||
|
||||
hideSchedules =
|
||||
$page.url.searchParams.get('hide_scheduled') != undefined
|
||||
? $page.url.searchParams.get('hide_scheduled') == 'true'
|
||||
: false
|
||||
|
||||
argFilter = $page.url.searchParams.get('arg')
|
||||
? JSON.parse(decodeURIComponent($page.url.searchParams.get('arg') ?? '{}'))
|
||||
: undefined
|
||||
resultFilter = $page.url.searchParams.get('result')
|
||||
? JSON.parse(decodeURIComponent($page.url.searchParams.get('result') ?? '{}'))
|
||||
: undefined
|
||||
|
||||
schedulePath = $page.url.searchParams.get('schedule_path') ?? undefined
|
||||
jobKindsCat = $page.url.searchParams.get('job_kinds') ?? 'runs'
|
||||
|
||||
// Handled on the main page
|
||||
minTs = $page.url.searchParams.get('min_ts') ?? undefined
|
||||
}
|
||||
|
||||
async function loadUsernames(): Promise<void> {
|
||||
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
async function loadFolders(): Promise<void> {
|
||||
folders = await FolderService.listFolders({
|
||||
workspace: $workspaceStore!
|
||||
}).then((x) => x.map((y) => y.name))
|
||||
}
|
||||
|
||||
async function loadPaths() {
|
||||
const npaths_scripts = await ScriptService.listScriptPaths({ workspace: $workspaceStore ?? '' })
|
||||
const npaths_flows = await FlowService.listFlowPaths({ workspace: $workspaceStore ?? '' })
|
||||
paths = npaths_scripts.concat(npaths_flows).sort()
|
||||
}
|
||||
|
||||
function computeCompletedJobs() {
|
||||
completedJobs =
|
||||
jobs?.filter((x) => x.type == 'CompletedJob').map((x) => x as CompletedJob) ?? []
|
||||
@@ -284,18 +223,9 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mounted = true
|
||||
loadPaths()
|
||||
loadUsernames()
|
||||
loadFolders()
|
||||
|
||||
intervalId = setInterval(syncer, refreshRate)
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
|
||||
window.addEventListener('popstate', updateFiltersFromURL)
|
||||
return () => {
|
||||
window.removeEventListener('popstate', updateFiltersFromURL)
|
||||
window.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
import FlowProgressBar from '../flows/FlowProgressBar.svelte'
|
||||
import FlowStatusViewer from '../FlowStatusViewer.svelte'
|
||||
import DurationMs from '../DurationMs.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let id: string
|
||||
export let blankLink = false
|
||||
export let workspace: string | undefined
|
||||
|
||||
let job: Job | undefined = undefined
|
||||
let watchJob: (id: string) => Promise<void>
|
||||
@@ -34,8 +36,7 @@
|
||||
let viewTab = 'result'
|
||||
</script>
|
||||
|
||||
<TestJobLoader bind:job={currentJob} bind:watchJob on:done={onDone} />
|
||||
|
||||
<TestJobLoader workspaceOverride={workspace} bind:job={currentJob} bind:watchJob on:done={onDone} />
|
||||
<div class="p-4 flex flex-col gap-2 items-start h-full">
|
||||
{#if job}
|
||||
<div class="flex gap-2">
|
||||
@@ -52,6 +53,11 @@
|
||||
Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if workspace && $workspaceStore != workspace}
|
||||
<Badge large>
|
||||
{workspace}
|
||||
</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<a
|
||||
href="/run/{job?.id}?workspace={job?.workspace_id}"
|
||||
@@ -68,7 +74,9 @@
|
||||
<JobArgs args={job?.args} />
|
||||
</div>
|
||||
|
||||
<span class="font-semibold text-xs leading-6">Results</span>
|
||||
{#if job?.type === Job.type.COMPLETED_JOB}
|
||||
<span class="font-semibold text-xs leading-6">Results</span>
|
||||
{/if}
|
||||
|
||||
{#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
<div class="text-sm font-semibold text-tertiary mb-1">
|
||||
|
||||
@@ -8,61 +8,74 @@
|
||||
export let loading: boolean = false
|
||||
export let selectedManualDate = 0
|
||||
|
||||
const manualDates = [
|
||||
export function computeMinMax(): { minTs: string; maxTs: string } | undefined {
|
||||
return manualDates[selectedManualDate].computeMinMax()
|
||||
}
|
||||
|
||||
const manualDates: {
|
||||
label: string
|
||||
computeMinMax: () => { minTs: string; maxTs: string } | undefined
|
||||
}[] = [
|
||||
{
|
||||
label: 'Last 1000 runs',
|
||||
setMinMax: () => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
computeMinMax: () => {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Within 30 seconds',
|
||||
setMinMax: () => {
|
||||
minTs = new Date(new Date().getTime() - 30 * 1000).toISOString()
|
||||
maxTs = new Date().toISOString()
|
||||
computeMinMax: () => {
|
||||
let minTs = new Date(new Date().getTime() - 30 * 1000).toISOString()
|
||||
let maxTs = new Date().toISOString()
|
||||
return { minTs, maxTs }
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Within last minute',
|
||||
setMinMax: () => {
|
||||
minTs = new Date(new Date().getTime() - 60 * 1000).toISOString()
|
||||
maxTs = new Date().toISOString()
|
||||
computeMinMax: () => {
|
||||
let minTs = new Date(new Date().getTime() - 60 * 1000).toISOString()
|
||||
let maxTs = new Date().toISOString()
|
||||
return { minTs, maxTs }
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Within last 5 minutes',
|
||||
setMinMax: () => {
|
||||
minTs = new Date(new Date().getTime() - 5 * 60 * 1000).toISOString()
|
||||
maxTs = new Date().toISOString()
|
||||
computeMinMax: () => {
|
||||
let minTs = new Date(new Date().getTime() - 5 * 60 * 1000).toISOString()
|
||||
let maxTs = new Date().toISOString()
|
||||
return { minTs, maxTs }
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Within last 30 minutes',
|
||||
setMinMax: () => {
|
||||
minTs = new Date(new Date().getTime() - 30 * 60 * 1000).toISOString()
|
||||
maxTs = new Date().toISOString()
|
||||
computeMinMax: () => {
|
||||
let minTs = new Date(new Date().getTime() - 30 * 60 * 1000).toISOString()
|
||||
let maxTs = new Date().toISOString()
|
||||
return { minTs, maxTs }
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Within last 24 hours',
|
||||
setMinMax: () => {
|
||||
minTs = new Date(new Date().getTime() - 24 * 60 * 60 * 1000).toISOString()
|
||||
maxTs = new Date().toISOString()
|
||||
computeMinMax: () => {
|
||||
let minTs = new Date(new Date().getTime() - 24 * 60 * 60 * 1000).toISOString()
|
||||
let maxTs = new Date().toISOString()
|
||||
return { minTs, maxTs }
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Within last 7 days',
|
||||
setMinMax: () => {
|
||||
minTs = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
||||
maxTs = new Date().toISOString()
|
||||
computeMinMax: () => {
|
||||
let minTs = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
||||
let maxTs = new Date().toISOString()
|
||||
return { minTs, maxTs }
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Within last month',
|
||||
setMinMax: () => {
|
||||
minTs = new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||
maxTs = new Date().toISOString()
|
||||
computeMinMax: () => {
|
||||
let minTs = new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||
let maxTs = new Date().toISOString()
|
||||
return { minTs, maxTs }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -75,7 +88,11 @@
|
||||
size="xs"
|
||||
wrapperClasses="border rounded-md"
|
||||
on:click={() => {
|
||||
manualDates[selectedManualDate].setMinMax()
|
||||
const ts = computeMinMax()
|
||||
if (ts) {
|
||||
minTs = ts.minTs
|
||||
maxTs = ts.maxTs
|
||||
}
|
||||
dispatch('loadJobs')
|
||||
}}
|
||||
dropdownItems={[
|
||||
@@ -83,7 +100,14 @@
|
||||
label: d.label,
|
||||
onClick: () => {
|
||||
selectedManualDate = i
|
||||
d.setMinMax()
|
||||
const ts = d.computeMinMax()
|
||||
if (ts) {
|
||||
minTs = ts.minTs
|
||||
maxTs = ts.maxTs
|
||||
} else {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
}
|
||||
dispatch('loadJobs')
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { displayDate } from '$lib/utils'
|
||||
|
||||
let jobs: QueuedJob[] | undefined = undefined
|
||||
export let allWorkspaces: boolean = false
|
||||
|
||||
getQueuedJobs()
|
||||
async function getQueuedJobs() {
|
||||
@@ -13,7 +14,8 @@
|
||||
workspace: $workspaceStore ?? '',
|
||||
scheduledForBeforeNow: true,
|
||||
suspended: false,
|
||||
running: false
|
||||
running: false,
|
||||
allWorkspaces
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -35,7 +37,7 @@
|
||||
<div class="flex">
|
||||
<a
|
||||
target="_blank"
|
||||
href={`/run/${job.id}?workspace=${$workspaceStore}`}
|
||||
href={`/run/${job.id}?workspace=${job.workspace_id}`}
|
||||
class="flex flex-row gap-2 items-center font-mono mr-8"
|
||||
>{job.id} <ExternalLink size={10} />
|
||||
</a>
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
)}
|
||||
style="width: {containerWidth}px"
|
||||
on:click={() => {
|
||||
selectedId = job.id
|
||||
dispatch('select')
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import Label from '../Label.svelte'
|
||||
import Section from '../Section.svelte'
|
||||
import CloseButton from '../common/CloseButton.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
// Filters
|
||||
export let path: string | null = null
|
||||
@@ -29,6 +30,7 @@
|
||||
export let paths: string[] = []
|
||||
export let usernames: string[] = []
|
||||
export let folders: string[] = []
|
||||
export let allWorkspaces = false
|
||||
|
||||
let copyArgFilter = argFilter
|
||||
let copyResultFilter = resultFilter
|
||||
@@ -54,6 +56,16 @@
|
||||
<div class="flex gap-4">
|
||||
{#if !mobile}
|
||||
<div class="flex gap-2">
|
||||
{#if $workspaceStore == 'admins'}
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Workspaces</span>
|
||||
<ToggleButtonGroup bind:selected={allWorkspaces}>
|
||||
<ToggleButton value={false} label="Admins" />
|
||||
<ToggleButton value={true} label="All" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Filter by</span>
|
||||
<ToggleButtonGroup
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Popup from '../common/popup/Popup.svelte'
|
||||
|
||||
export let queue_count: Tweened<number> | undefined = undefined
|
||||
export let allWorkspaces: boolean = false
|
||||
</script>
|
||||
|
||||
<div class="flex gap-1 relative max-w-36 min-w-[50px] items-baseline">
|
||||
@@ -15,7 +16,7 @@
|
||||
<svelte:fragment slot="button">
|
||||
<span class="text-2xs truncate">jobs</span>
|
||||
</svelte:fragment>
|
||||
<QueuePopover />
|
||||
<QueuePopover {allWorkspaces} />
|
||||
</Popup>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import type { Job } from '$lib/gen'
|
||||
import RunRow from './RunRow.svelte'
|
||||
import VirtualList from 'svelte-tiny-virtual-list'
|
||||
import { onMount } from 'svelte'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
//import InfiniteLoading from 'svelte-infinite-loading'
|
||||
|
||||
export let jobs: Job[] = []
|
||||
export let jobs: Job[] | undefined = undefined
|
||||
export let selectedId: string | undefined = undefined
|
||||
export let selectedWorkspace: string | undefined = undefined
|
||||
// const loadMoreQuantity: number = 100
|
||||
|
||||
function getTime(job: Job): string | undefined {
|
||||
@@ -56,7 +57,7 @@
|
||||
return sortedLogs
|
||||
}
|
||||
|
||||
$: groupedJobs = groupJobsByDay(jobs)
|
||||
$: groupedJobs = jobs ? groupJobsByDay(jobs) : undefined
|
||||
|
||||
type FlatJobs =
|
||||
| {
|
||||
@@ -81,14 +82,14 @@
|
||||
return flatJobs
|
||||
}
|
||||
|
||||
$: flatJobs = flattenJobs(groupedJobs)
|
||||
$: flatJobs = groupedJobs ? flattenJobs(groupedJobs) : undefined
|
||||
|
||||
let stickyIndices: number[] = []
|
||||
|
||||
$: {
|
||||
stickyIndices = []
|
||||
let index = 0
|
||||
for (const entry of flatJobs) {
|
||||
for (const entry of flatJobs ?? []) {
|
||||
if (entry.type === 'date') {
|
||||
stickyIndices.push(index)
|
||||
}
|
||||
@@ -117,17 +118,29 @@
|
||||
}
|
||||
*/
|
||||
|
||||
onMount(() => {
|
||||
function computeHeight() {
|
||||
tableHeight = document.querySelector('#runs-table-wrapper')!.parentElement?.clientHeight ?? 0
|
||||
}
|
||||
onMount(() => {
|
||||
computeHeight()
|
||||
})
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="divide-y min-w-[640px]" id="runs-table-wrapper" bind:clientWidth={containerWidth}>
|
||||
<svelte:window on:resize={() => computeHeight()} />
|
||||
|
||||
<div
|
||||
class="divide-y min-w-[640px] h-full"
|
||||
id="runs-table-wrapper"
|
||||
bind:clientWidth={containerWidth}
|
||||
>
|
||||
<div
|
||||
class="flex flex-row bg-surface-secondary sticky top-0 w-full p-2 pr-4"
|
||||
bind:clientHeight={header}
|
||||
>
|
||||
<div class="w-1/12" />
|
||||
<div class="w-1/12 text-2xs"
|
||||
>{jobs?.length == 1000 ? '1000+' : jobs ? jobs.length.toString() : '...'} jobs</div
|
||||
>
|
||||
<div class="w-4/12 text-xs font-semibold">Timestamp</div>
|
||||
<div class="w-4/12 text-xs font-semibold">Path</div>
|
||||
<div class="w-3/12 text-xs font-semibold">Triggered by</div>
|
||||
@@ -136,44 +149,48 @@
|
||||
<VirtualList
|
||||
width="100%"
|
||||
height={tableHeight - header}
|
||||
itemCount={flatJobs.length}
|
||||
itemCount={flatJobs?.length ?? 3}
|
||||
itemSize={42}
|
||||
{stickyIndices}
|
||||
>
|
||||
<div slot="item" let:index let:style {style} class="w-full">
|
||||
{@const jobOrDate = flatJobs[index]}
|
||||
{#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>
|
||||
{#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
|
||||
job={jobOrDate.job}
|
||||
{selectedId}
|
||||
on:select={() => {
|
||||
selectedWorkspace = jobOrDate.job.workspace_id
|
||||
selectedId = jobOrDate.job.id
|
||||
dispatch('select')
|
||||
}}
|
||||
on:filterByPath
|
||||
on:filterByUser
|
||||
on:filterByFolder
|
||||
{containerWidth}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-row items-center h-full w-full">
|
||||
<RunRow
|
||||
job={jobOrDate.job}
|
||||
bind:selectedId
|
||||
on:select
|
||||
on:filterByPath
|
||||
on:filterByUser
|
||||
on:filterByFolder
|
||||
{containerWidth}
|
||||
/>
|
||||
</div>
|
||||
{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}
|
||||
</div>
|
||||
<!-- <div slot="footer">
|
||||
<InfiniteLoading on:infinite={infiniteHandler}>
|
||||
<div slot="noMore">
|
||||
<div class="text-center text-xs text-secondary p-2">
|
||||
Reached the limit of {MAX_ITEMS} jobs. Please refine your search using filters.
|
||||
</div>
|
||||
</div>
|
||||
</InfiniteLoading>
|
||||
</div> -->
|
||||
</VirtualList>
|
||||
</div>
|
||||
{#if jobs?.length == 0}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { JobService, Job, CompletedJob } from '$lib/gen'
|
||||
import {
|
||||
JobService,
|
||||
Job,
|
||||
CompletedJob,
|
||||
UserService,
|
||||
FolderService,
|
||||
ScriptService,
|
||||
FlowService
|
||||
} from '$lib/gen'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -25,6 +33,7 @@
|
||||
|
||||
let jobs: Job[] | undefined
|
||||
let selectedId: string | undefined = undefined
|
||||
let selectedWorkspace: string | undefined = undefined
|
||||
|
||||
// All Filters
|
||||
// Filter by
|
||||
@@ -57,6 +66,8 @@
|
||||
let maxTs = $page.url.searchParams.get('max_ts') ?? undefined
|
||||
let schedulePath = $page.url.searchParams.get('schedule_path') ?? undefined
|
||||
let jobKindsCat = $page.url.searchParams.get('job_kinds') ?? 'runs'
|
||||
let allWorkspaces = $page.url.searchParams.get('all_workspaces') == 'true' ?? false
|
||||
|
||||
let queue_count: Tweened<number> | undefined = undefined
|
||||
let jobKinds: string | undefined = undefined
|
||||
let loading: boolean = false
|
||||
@@ -74,11 +85,109 @@
|
||||
let innerWidth = window.innerWidth
|
||||
let jobLoader: JobLoader | undefined = undefined
|
||||
|
||||
let manualDatePicker: ManuelDatePicker
|
||||
|
||||
$: (user ||
|
||||
folder ||
|
||||
path ||
|
||||
success !== undefined ||
|
||||
isSkipped ||
|
||||
hideSchedules ||
|
||||
argFilter ||
|
||||
resultFilter ||
|
||||
schedulePath ||
|
||||
jobKindsCat ||
|
||||
minTs ||
|
||||
maxTs ||
|
||||
allWorkspaces ||
|
||||
$workspaceStore) &&
|
||||
setQuery()
|
||||
|
||||
function setQuery() {
|
||||
let searchParams = new URLSearchParams()
|
||||
|
||||
if (user) {
|
||||
searchParams.set('user', user)
|
||||
} else {
|
||||
searchParams.delete('user')
|
||||
}
|
||||
|
||||
if (folder) {
|
||||
searchParams.set('folder', folder)
|
||||
} else {
|
||||
searchParams.delete('folder')
|
||||
}
|
||||
|
||||
if (success !== undefined) {
|
||||
searchParams.set('success', success.toString())
|
||||
} else {
|
||||
searchParams.delete('success')
|
||||
}
|
||||
|
||||
if (isSkipped) {
|
||||
searchParams.set('is_skipped', isSkipped.toString())
|
||||
} else {
|
||||
searchParams.delete('is_skipped')
|
||||
}
|
||||
|
||||
if (hideSchedules) {
|
||||
searchParams.set('hide_scheduled', hideSchedules.toString())
|
||||
} else {
|
||||
searchParams.delete('hide_scheduled')
|
||||
}
|
||||
|
||||
if (allWorkspaces && $workspaceStore == 'admins') {
|
||||
searchParams.set('all_workspaces', allWorkspaces.toString())
|
||||
searchParams.set('workspace', 'admins')
|
||||
} else {
|
||||
searchParams.delete('all_workspaces')
|
||||
}
|
||||
|
||||
// ArgFilter is an object. Encode it to a string
|
||||
if (argFilter) {
|
||||
searchParams.set('arg', encodeURIComponent(JSON.stringify(argFilter)))
|
||||
} else {
|
||||
searchParams.delete('arg')
|
||||
}
|
||||
|
||||
if (resultFilter) {
|
||||
searchParams.set('result', encodeURIComponent(JSON.stringify(resultFilter)))
|
||||
} else {
|
||||
searchParams.delete('result')
|
||||
}
|
||||
if (schedulePath) {
|
||||
searchParams.set('schedule_path', schedulePath)
|
||||
} else {
|
||||
searchParams.delete('schedule_path')
|
||||
}
|
||||
if (jobKindsCat != 'runs') {
|
||||
searchParams.set('job_kinds', jobKindsCat)
|
||||
} else {
|
||||
searchParams.delete('job_kinds')
|
||||
}
|
||||
|
||||
if (minTs) {
|
||||
searchParams.set('min_ts', minTs)
|
||||
} else {
|
||||
searchParams.delete('min_ts')
|
||||
}
|
||||
|
||||
if (maxTs) {
|
||||
searchParams.set('max_ts', maxTs)
|
||||
} else {
|
||||
searchParams.delete('max_ts')
|
||||
}
|
||||
|
||||
let newPath = path ? `/${path}` : '/'
|
||||
let newUrl = `/runs${newPath}?${searchParams.toString()}`
|
||||
history.replaceState(history.state, '', newUrl.toString())
|
||||
}
|
||||
|
||||
function reloadLogsWithoutFilterError() {
|
||||
if (resultError == '' && argError == '') {
|
||||
filterTimeout && clearTimeout(filterTimeout)
|
||||
filterTimeout = setTimeout(() => {
|
||||
jobLoader?.loadJobs(true)
|
||||
jobLoader?.loadJobs(minTs, maxTs, true)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
@@ -92,32 +201,54 @@
|
||||
completedJobs = undefined
|
||||
selectedManualDate = 0
|
||||
selectedId = undefined
|
||||
jobLoader?.loadJobs(true)
|
||||
selectedWorkspace = undefined
|
||||
jobLoader?.loadJobs(minTs, maxTs, true)
|
||||
}
|
||||
|
||||
async function loadUsernames(): Promise<void> {
|
||||
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
|
||||
}
|
||||
|
||||
async function loadFolders(): Promise<void> {
|
||||
folders = await FolderService.listFolders({
|
||||
workspace: $workspaceStore!
|
||||
}).then((x) => x.map((y) => y.name))
|
||||
}
|
||||
|
||||
async function loadPaths() {
|
||||
const npaths_scripts = await ScriptService.listScriptPaths({ workspace: $workspaceStore ?? '' })
|
||||
const npaths_flows = await FlowService.listFlowPaths({ workspace: $workspaceStore ?? '' })
|
||||
paths = npaths_scripts.concat(npaths_flows).sort()
|
||||
}
|
||||
|
||||
$: if ($workspaceStore) {
|
||||
loadUsernames()
|
||||
loadFolders()
|
||||
loadPaths()
|
||||
}
|
||||
</script>
|
||||
|
||||
<JobLoader
|
||||
{allWorkspaces}
|
||||
bind:jobs
|
||||
bind:user
|
||||
bind:folder
|
||||
bind:path
|
||||
bind:success
|
||||
bind:isSkipped
|
||||
bind:argFilter
|
||||
bind:resultFilter
|
||||
bind:schedulePath
|
||||
bind:jobKindsCat
|
||||
{user}
|
||||
{folder}
|
||||
{path}
|
||||
{success}
|
||||
{isSkipped}
|
||||
{argFilter}
|
||||
{resultFilter}
|
||||
{schedulePath}
|
||||
{jobKindsCat}
|
||||
computeMinAndMax={manualDatePicker?.computeMinMax}
|
||||
bind:minTs
|
||||
bind:maxTs
|
||||
bind:jobKinds
|
||||
{jobKinds}
|
||||
bind:queue_count
|
||||
bind:autoRefresh
|
||||
bind:paths
|
||||
bind:usernames
|
||||
bind:folders
|
||||
{autoRefresh}
|
||||
bind:completedJobs
|
||||
bind:argError
|
||||
bind:resultError
|
||||
{argError}
|
||||
{resultError}
|
||||
bind:loading
|
||||
bind:this={jobLoader}
|
||||
/>
|
||||
@@ -129,7 +260,7 @@
|
||||
on:confirmed={async () => {
|
||||
cancelAllJobs = false
|
||||
let uuids = await JobService.cancelAll({ workspace: $workspaceStore ?? '' })
|
||||
jobLoader?.loadJobs(true)
|
||||
jobLoader?.loadJobs(minTs, maxTs, true, true)
|
||||
sendUserToast(`Canceled ${uuids.length} jobs`)
|
||||
}}
|
||||
on:canceled={() => {
|
||||
@@ -140,7 +271,7 @@
|
||||
<Drawer bind:this={runDrawer}>
|
||||
<DrawerContent title="Run details" on:close={runDrawer.closeDrawer}>
|
||||
{#if selectedId}
|
||||
<JobPreview blankLink id={selectedId} />
|
||||
<JobPreview blankLink id={selectedId} workspace={selectedWorkspace} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -180,6 +311,7 @@
|
||||
bind:resultError
|
||||
bind:jobKindsCat
|
||||
bind:hideSchedules
|
||||
bind:allWorkspaces
|
||||
on:change={reloadLogsWithoutFilterError}
|
||||
{usernames}
|
||||
{folders}
|
||||
@@ -195,12 +327,13 @@
|
||||
on:zoom={async (e) => {
|
||||
minTs = e.detail.min.toISOString()
|
||||
maxTs = e.detail.max.toISOString()
|
||||
jobLoader?.loadJobs(minTs, maxTs, true)
|
||||
}}
|
||||
/>
|
||||
</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} />
|
||||
<RunsQueue {queue_count} {allWorkspaces} />
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
@@ -246,7 +379,16 @@
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Button size="xs" color="light" variant="border" on:click={reset}>Reset</Button>
|
||||
<ManuelDatePicker bind:minTs bind:maxTs bind:selectedManualDate {loading} />
|
||||
<ManuelDatePicker
|
||||
on:loadJobs={() => {
|
||||
jobLoader?.loadJobs(minTs, maxTs, true, true)
|
||||
}}
|
||||
bind:minTs
|
||||
bind:maxTs
|
||||
bind:selectedManualDate
|
||||
{loading}
|
||||
bind:this={manualDatePicker}
|
||||
/>
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={autoRefresh}
|
||||
@@ -263,6 +405,7 @@
|
||||
<RunsTable
|
||||
{jobs}
|
||||
bind:selectedId
|
||||
bind:selectedWorkspace
|
||||
on:filterByPath={(e) => {
|
||||
user = null
|
||||
folder = null
|
||||
@@ -289,7 +432,7 @@
|
||||
</Pane>
|
||||
<Pane size={40} minSize={15} class="border-t">
|
||||
{#if selectedId}
|
||||
<JobPreview id={selectedId} />
|
||||
<JobPreview id={selectedId} workspace={selectedWorkspace} />
|
||||
{:else}
|
||||
<div class="text-xs m-4">No job selected</div>
|
||||
{/if}
|
||||
@@ -329,6 +472,7 @@
|
||||
bind:argError
|
||||
bind:resultError
|
||||
bind:hideSchedules
|
||||
bind:allWorkspaces
|
||||
mobile={true}
|
||||
on:change={reloadLogsWithoutFilterError}
|
||||
/>
|
||||
@@ -341,13 +485,14 @@
|
||||
on:zoom={async (e) => {
|
||||
minTs = e.detail.min.toISOString()
|
||||
maxTs = e.detail.max.toISOString()
|
||||
jobLoader?.loadJobs(minTs, maxTs, true)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 md:flex-row w-full p-4">
|
||||
<div class="flex items-center flex-row gap-2 grow mb-4">
|
||||
{#if queue_count}
|
||||
<RunsQueue {queue_count} />
|
||||
<RunsQueue {queue_count} {allWorkspaces} />
|
||||
{/if}
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -394,7 +539,16 @@
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Button size="xs" color="light" variant="border" on:click={reset}>Reset</Button>
|
||||
<ManuelDatePicker bind:minTs bind:maxTs bind:selectedManualDate {loading} />
|
||||
<ManuelDatePicker
|
||||
on:loadJobs={() => {
|
||||
jobLoader?.loadJobs(minTs, maxTs, true, true)
|
||||
}}
|
||||
bind:this={manualDatePicker}
|
||||
bind:minTs
|
||||
bind:maxTs
|
||||
bind:selectedManualDate
|
||||
{loading}
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
size="xs"
|
||||
@@ -408,6 +562,7 @@
|
||||
<RunsTable
|
||||
{jobs}
|
||||
bind:selectedId
|
||||
bind:selectedWorkspace
|
||||
on:select={() => {
|
||||
runDrawer.openDrawer()
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user