mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-08 00:03:07 +00:00
feat: rework schedule page entirely to display jobs informations
This commit is contained in:
@@ -604,6 +604,112 @@
|
||||
},
|
||||
"query": "SELECT * FROM workspace LIMIT $1 OFFSET $2"
|
||||
},
|
||||
"15e143ab2dc6368238849beb3ee3969cd601b060b5d58d9831e6be7008156d26": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"ordinal": 1,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "edited_by",
|
||||
"ordinal": 2,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "edited_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "schedule",
|
||||
"ordinal": 4,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"ordinal": 5,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "script_path",
|
||||
"ordinal": 6,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"ordinal": 7,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "extra_perms",
|
||||
"ordinal": 8,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "is_flow",
|
||||
"ordinal": 9,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"ordinal": 10,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "timezone",
|
||||
"ordinal": 12,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "on_failure",
|
||||
"ordinal": 13,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "jobs",
|
||||
"ordinal": 14,
|
||||
"type_info": "JsonArray"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT schedule.*, t.jobs FROM schedule, LATERAL ( SELECT ARRAY (SELECT json_build_object('id', id, 'success', success, 'duration_ms', duration_ms) FROM completed_job WHERE\n completed_job.schedule_path = schedule_path AND schedule.workspace_id = completed_job.workspace_id AND parent_job IS NULL ORDER BY created_at DESC LIMIT 20) AS jobs ) t\n WHERE schedule.workspace_id = $1 ORDER BY schedule.edited_at desc LIMIT $2 OFFSET $3"
|
||||
},
|
||||
"163f00eb8b1a489d5f382cdba22a5744e88a8e6f1532d7cb02af560f5f5d49f7": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
||||
@@ -3555,6 +3555,7 @@ paths:
|
||||
- $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"
|
||||
@@ -3588,6 +3589,7 @@ paths:
|
||||
- $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"
|
||||
@@ -3628,6 +3630,7 @@ paths:
|
||||
- $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"
|
||||
@@ -4293,6 +4296,26 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/Schedule"
|
||||
|
||||
/w/{workspace}/schedules/list_with_jobs:
|
||||
get:
|
||||
summary: list schedules with last 20 jobs
|
||||
operationId: listSchedulesWithJobs
|
||||
tags:
|
||||
- schedule
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: schedule list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ScheduleWJobs"
|
||||
|
||||
/w/{workspace}/groups/list:
|
||||
get:
|
||||
summary: list groups
|
||||
@@ -5216,6 +5239,12 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
SchedulePath:
|
||||
name: schedule_path
|
||||
description: mask to filter by schedule path
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
ScriptExactPath:
|
||||
name: script_path_exact
|
||||
description: mask to filter exact matching path
|
||||
@@ -6285,6 +6314,27 @@ components:
|
||||
- enabled
|
||||
- email
|
||||
|
||||
ScheduleWJobs:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/Schedule"
|
||||
- type: object
|
||||
properties:
|
||||
jobs:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
success:
|
||||
type: boolean
|
||||
duration_ms:
|
||||
type: number
|
||||
required:
|
||||
- id
|
||||
- success
|
||||
- duration_ms
|
||||
|
||||
NewSchedule:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -431,6 +431,7 @@ pub struct ListQueueQuery {
|
||||
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub running: Option<bool>,
|
||||
pub schedule_path: Option<String>,
|
||||
pub parent_job: Option<String>,
|
||||
pub order_desc: Option<bool>,
|
||||
pub job_kinds: Option<String>,
|
||||
@@ -454,6 +455,9 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
||||
if let Some(p) = &lq.script_path_exact {
|
||||
sqlb.and_where_eq("script_path", "?".bind(p));
|
||||
}
|
||||
if let Some(p) = &lq.schedule_path {
|
||||
sqlb.and_where_eq("schedule_path", "?".bind(p));
|
||||
}
|
||||
if let Some(h) = &lq.script_hash {
|
||||
sqlb.and_where_eq("script_hash", "?".bind(h));
|
||||
}
|
||||
@@ -597,6 +601,7 @@ async fn list_jobs(
|
||||
suspended: lq.suspended,
|
||||
args: lq.args,
|
||||
tag: lq.tag,
|
||||
schedule_path: lq.schedule_path,
|
||||
},
|
||||
&[
|
||||
"'QueuedJob' as typ",
|
||||
@@ -610,7 +615,7 @@ async fn list_jobs(
|
||||
"running",
|
||||
"script_hash",
|
||||
"script_path",
|
||||
"CASE WHEN pg_column_size(args) > 1000 THEN '\"too large args\"'::jsonb ELSE args END",
|
||||
"null as args",
|
||||
"null as duration_ms",
|
||||
"null as success",
|
||||
"false as deleted",
|
||||
@@ -646,7 +651,7 @@ async fn list_jobs(
|
||||
"null as running",
|
||||
"script_hash",
|
||||
"script_path",
|
||||
"CASE WHEN pg_column_size(args) > 1000 THEN '\"too large args\"'::jsonb ELSE args END",
|
||||
"null as args",
|
||||
"duration_ms",
|
||||
"success",
|
||||
"deleted",
|
||||
@@ -2123,6 +2128,10 @@ fn list_completed_jobs_query(
|
||||
.limit(per_page)
|
||||
.clone();
|
||||
|
||||
if let Some(p) = &lq.schedule_path {
|
||||
sqlb.and_where_eq("schedule_path", "?".bind(p));
|
||||
}
|
||||
|
||||
if let Some(ps) = &lq.script_path_start {
|
||||
sqlb.and_where_like_left("script_path", "?".bind(ps));
|
||||
}
|
||||
@@ -2188,6 +2197,7 @@ pub struct ListCompletedQuery {
|
||||
pub is_skipped: Option<bool>,
|
||||
pub is_flow_step: Option<bool>,
|
||||
pub suspended: Option<bool>,
|
||||
pub schedule_path: Option<String>,
|
||||
// filter by matching a subset of the args using base64 encoded json subset
|
||||
pub args: Option<String>,
|
||||
// filter by matching a subset of the result using base64 encoded json subset
|
||||
|
||||
@@ -16,7 +16,7 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Postgres, Transaction};
|
||||
use std::str::FromStr;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
@@ -31,6 +31,7 @@ use windmill_queue::{self, schedule::push_scheduled_job, QueueTransaction};
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
.route("/list", get(list_schedule))
|
||||
.route("/list_with_jobs", get(list_schedule_with_jobs))
|
||||
.route("/get/*path", get(get_schedule))
|
||||
.route("/exists/*path", get(exists_schedule))
|
||||
.route("/create", post(create_schedule))
|
||||
@@ -231,6 +232,57 @@ async fn list_schedule(
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ScheduleWJobs {
|
||||
pub workspace_id: String,
|
||||
pub path: String,
|
||||
pub edited_by: String,
|
||||
pub edited_at: DateTime<chrono::Utc>,
|
||||
pub schedule: String,
|
||||
pub timezone: String,
|
||||
pub enabled: bool,
|
||||
pub script_path: String,
|
||||
pub is_flow: bool,
|
||||
pub args: Option<serde_json::Value>,
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub email: String,
|
||||
pub error: Option<String>,
|
||||
pub on_failure: Option<String>,
|
||||
pub jobs: Option<Vec<serde_json::Value>>,
|
||||
}
|
||||
|
||||
async fn list_schedule_with_jobs(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> JsonResult<Vec<ScheduleWJobs>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
let rows = sqlx::query_as!(ScheduleWJobs,
|
||||
"SELECT schedule.*, t.jobs FROM schedule, LATERAL ( SELECT ARRAY (SELECT json_build_object('id', id, 'success', success, 'duration_ms', duration_ms) FROM completed_job WHERE
|
||||
completed_job.schedule_path = schedule_path AND schedule.workspace_id = completed_job.workspace_id AND parent_job IS NULL ORDER BY created_at DESC LIMIT 20) AS jobs ) t
|
||||
WHERE schedule.workspace_id = $1 ORDER BY schedule.edited_at desc LIMIT $2 OFFSET $3",
|
||||
w_id,
|
||||
per_page as i64,
|
||||
offset as i64
|
||||
)
|
||||
.fetch_all(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
// SELECT id, title AS item_title, t.tag_array
|
||||
// FROM items i, LATERAL ( -- this is an implicit CROSS JOIN
|
||||
// SELECT ARRAY (
|
||||
// SELECT t.title
|
||||
// FROM items_tags it
|
||||
// JOIN tags t ON t.id = it.tag_id
|
||||
// WHERE it.item_id = i.id
|
||||
// ) AS tag_array
|
||||
// ) t;
|
||||
|
||||
async fn get_schedule(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
export let color: BadgeColor = 'gray'
|
||||
export let large = false
|
||||
export let small = false
|
||||
export let href = ''
|
||||
export let rounded = false
|
||||
export let dismissable = false
|
||||
@@ -45,7 +46,7 @@
|
||||
|
||||
$: badgeClass = classNames(
|
||||
baseClass,
|
||||
large ? 'text-sm font-medium' : 'text-xs font-semibold',
|
||||
small ? 'text-xs' : large ? 'text-sm font-medium' : 'text-xs font-semibold',
|
||||
colors[color],
|
||||
href &&
|
||||
(color.startsWith(ColorModifier) ? hovers[color.replace(ColorModifier, '')] : hovers[color]),
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row space-x-2">
|
||||
<JobPreview {job}>
|
||||
<JobPreview id={job.id}>
|
||||
<div class="whitespace-nowrap">
|
||||
{#if job.script_path}
|
||||
<a href="/run/{job.id}?workspace={job.workspace_id}">{job.script_path} </a>
|
||||
|
||||
@@ -11,33 +11,33 @@
|
||||
import JobArgs from '../JobArgs.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import LogViewer from '../LogViewer.svelte'
|
||||
import { forLater } from '$lib/utils'
|
||||
import { forLater, msToSec } from '$lib/utils'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { faHourglassHalf } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Badge } from '../common'
|
||||
|
||||
const POPUP_HEIGHT = 240 as const
|
||||
|
||||
export let job: Job | undefined
|
||||
export let id: string
|
||||
let job: Job | undefined = undefined
|
||||
let hovered = false
|
||||
let timeout: NodeJS.Timeout | undefined
|
||||
let watchJob: (id: string) => Promise<void>
|
||||
let args = job?.args
|
||||
let result: any
|
||||
let loaded = false
|
||||
let wrapper: HTMLElement
|
||||
let popupOnTop = true
|
||||
|
||||
$: open = $openStore === job?.id
|
||||
$: open = $openStore === id
|
||||
|
||||
async function instantOpen() {
|
||||
if (!open) {
|
||||
hovered = true
|
||||
if (!job) {
|
||||
return
|
||||
}
|
||||
popupOnTop = wrapper.getBoundingClientRect().top > POPUP_HEIGHT
|
||||
openStore.set(job.id)
|
||||
openStore.set(id)
|
||||
if (!loaded) {
|
||||
await tick()
|
||||
watchJob && watchJob(job.id)
|
||||
watchJob && watchJob(id)
|
||||
}
|
||||
} else {
|
||||
timeout && clearTimeout(timeout)
|
||||
@@ -99,8 +99,18 @@
|
||||
border border-gray-300 shadow-xl flex justify-start items-start w-[600px] h-80
|
||||
overflow-hidden"
|
||||
>
|
||||
<div class="w-1/2 h-full overflow-auto px-2">
|
||||
<JobArgs {args} tableClass="!pt-0 !min-w-0 !block" />
|
||||
<div class="absolute bottom-0 right-1 flex justify-end gap-2 pb-0.5">
|
||||
<Badge>
|
||||
Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'}
|
||||
</Badge>
|
||||
<Badge>
|
||||
<Icon class="text-gray-700" data={faHourglassHalf} scale={0.5} /><span class="mx-2">
|
||||
Ran in {msToSec(job?.['duration_ms'])}s</span
|
||||
>
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="w-1/2 h-full overflow-auto px-2 -mt-3">
|
||||
<JobArgs args={job?.args} tableClass="!pt-0 !min-w-0 !block" />
|
||||
</div>
|
||||
<div class="w-1/2 h-full overflow-auto p-2">
|
||||
{#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { JobService, Job, CompletedJob, ScriptService, FlowService } from '$lib/gen'
|
||||
import { setQueryWithoutLoad } from '$lib/utils'
|
||||
import { setQueryWithoutLoad } from '$lib/utils'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -40,6 +40,7 @@
|
||||
let resultFilter: any = $page.url.searchParams.get('result') ?? undefined
|
||||
let minTs = $page.url.searchParams.get('min_ts') ?? undefined
|
||||
let maxTs = $page.url.searchParams.get('max_ts') ?? undefined
|
||||
let schedulePath = $page.url.searchParams.get('schedule_path') ?? undefined
|
||||
|
||||
let nbOfJobs = 30
|
||||
|
||||
@@ -81,6 +82,7 @@
|
||||
workspace: $workspaceStore!,
|
||||
startedBefore,
|
||||
startedAfter,
|
||||
schedulePath,
|
||||
scriptPathExact: path === '' ? undefined : path,
|
||||
jobKinds,
|
||||
success,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ScheduleService, type Schedule, JobService, type ScriptArgs } from '$lib/gen'
|
||||
import { ScheduleService, JobService, type ScriptArgs, type ScheduleWJobs } from '$lib/gen'
|
||||
import { canWrite, displayDate } from '$lib/utils'
|
||||
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
@@ -10,34 +10,39 @@
|
||||
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import TableCustom from '$lib/components/TableCustom.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
faCircle,
|
||||
faEdit,
|
||||
faEye,
|
||||
faList,
|
||||
faPen,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faShare,
|
||||
faToggleOff,
|
||||
faToggleOn,
|
||||
faTrash
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { goto } from '$app/navigation'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
|
||||
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
|
||||
import JobPreview from '$lib/components/jobs/JobPreview.svelte'
|
||||
|
||||
type ScheduleW = Schedule & { canWrite: boolean }
|
||||
type ScheduleW = ScheduleWJobs & { canWrite: boolean }
|
||||
|
||||
let schedules: ScheduleW[] = []
|
||||
let shareModal: ShareModal
|
||||
let loading = true
|
||||
|
||||
async function loadSchedules(): Promise<void> {
|
||||
schedules = (await ScheduleService.listSchedules({ workspace: $workspaceStore! })).map((x) => {
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
schedules = (await ScheduleService.listSchedulesWithJobs({ workspace: $workspaceStore! })).map(
|
||||
(x) => {
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
}
|
||||
)
|
||||
loading = false
|
||||
}
|
||||
|
||||
@@ -86,9 +91,20 @@
|
||||
}
|
||||
}
|
||||
let scheduleEditor: ScheduleEditor
|
||||
|
||||
let filteredItems: (ScheduleW & { marked?: any })[] | undefined = []
|
||||
let filter = ''
|
||||
</script>
|
||||
|
||||
<ScheduleEditor on:update={loadSchedules} bind:this={scheduleEditor} />
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={schedules}
|
||||
bind:filteredItems
|
||||
f={(x) => x.path + ' ' + x.script_path}
|
||||
/>
|
||||
|
||||
<CenteredPage>
|
||||
<PageHeader
|
||||
title="Schedules"
|
||||
@@ -99,169 +115,198 @@
|
||||
New schedule
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<div class="mt-10 pb-60 overflow-auto">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="w-12/12 pb-4 pt-6">
|
||||
<input type="text" placeholder="Search schedule" bind:value={filter} class="search-item" />
|
||||
</div>
|
||||
{#if loading}
|
||||
<Skeleton layout={[0.5, [2.1], 0.7]} />
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[4], 0.7]} />
|
||||
<Skeleton layout={[[6], 0.4]} />
|
||||
{/each}
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th class="!px-0" />
|
||||
<th>Schedule</th>
|
||||
<th>Script/Flow</th>
|
||||
<th>Schedule</th>
|
||||
<th>Timezone</th>
|
||||
<th />
|
||||
<th>Enabled</th>
|
||||
<th>Last Edit</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each schedules as { path, error, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, extra_perms, canWrite, args }}
|
||||
<tr class={enabled ? '' : 'bg-gray-50'}>
|
||||
<td class="!px-0 text-center">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</td>
|
||||
<td class="max-w-sm min-w-[100px]">
|
||||
<button
|
||||
class="break-all text-left text-sm text-blue-600 font-normal"
|
||||
on:click={() => scheduleEditor?.openEdit(path, is_flow)}
|
||||
>
|
||||
{path}
|
||||
</button>
|
||||
</td>
|
||||
<td class="">
|
||||
<div class="inline-flex flex-row gap-x-2 align-middle w-full">
|
||||
<div class="grow !min-w-[100px]">
|
||||
<a
|
||||
class="text-sm break-all"
|
||||
href="{is_flow ? '/flows/get' : '/scripts/get'}/{script_path}"
|
||||
>
|
||||
{script_path}
|
||||
</a>
|
||||
</div>
|
||||
<Badge class="text-2xs font-mono">{is_flow ? 'flow' : 'script'}</Badge>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<Badge color="blue">{schedule}</Badge>
|
||||
</td>
|
||||
<td>
|
||||
<Badge color="blue">{timezone}</Badge>
|
||||
</td>
|
||||
<td>
|
||||
<div class="w-10">
|
||||
{#if error}
|
||||
<Popover notClickable>
|
||||
<span class="flex h-4 w-4">
|
||||
<Icon
|
||||
class="text-red-600 animate-ping absolute inline-flex "
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during last job scheduling"
|
||||
/>
|
||||
<Icon
|
||||
class="text-red-600 relative inline-flex"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during last job scheduling"
|
||||
/>
|
||||
</span>
|
||||
<div slot="text">
|
||||
The schedule disabled itself because there was an error scheduling the next
|
||||
job: {error}
|
||||
</div>
|
||||
</Popover>
|
||||
{:else if !schedules?.length}
|
||||
<div class="text-center text-sm text-gray-600 mt-2"> No schedules </div>
|
||||
{:else if filteredItems?.length}
|
||||
<div class="border rounded-md divide-y divide-gray-200">
|
||||
{#each filteredItems as { path, error, edited_by, edited_at, schedule, timezone, enabled, script_path, is_flow, extra_perms, canWrite, args, marked, jobs }}
|
||||
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
|
||||
{@const avg_s = jobs
|
||||
? jobs.reduce((acc, x) => acc + x.duration_ms, 0) / jobs.length
|
||||
: undefined}
|
||||
|
||||
<div
|
||||
class="hover:bg-gray-50 w-full items-center px-4 py-2 gap-4 first-of-type:!border-t-0
|
||||
first-of-type:rounded-t-md last-of-type:rounded-b-md flex flex-col"
|
||||
>
|
||||
<div class="w-full flex gap-5 items-center">
|
||||
<RowIcon kind={is_flow ? 'flow' : 'script'} />
|
||||
|
||||
<a
|
||||
href="#{path}"
|
||||
on:click={() => scheduleEditor?.openEdit(path, is_flow)}
|
||||
class="min-w-0 grow hover:underline decoration-gray-400"
|
||||
>
|
||||
<div class="text-gray-900 flex-wrap text-left text-md font-semibold mb-1 truncate">
|
||||
{#if marked}
|
||||
<span class="text-xs">
|
||||
{@html marked}
|
||||
</span>
|
||||
{:else}
|
||||
{script_path}
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<Toggle
|
||||
checked={enabled}
|
||||
on:change={(e) => {
|
||||
if (canWrite) {
|
||||
setScheduleEnabled(path, e.detail)
|
||||
} else {
|
||||
sendUserToast('not enough permission', true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td class="min-w-[100px]">
|
||||
<span class="text-2xs">By {edited_by} <br />the {displayDate(edited_at)}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="inline-flex gap-2">
|
||||
<Button
|
||||
href={`/runs/${script_path}`}
|
||||
size="xs"
|
||||
startIcon={{ icon: faList }}
|
||||
color="light"
|
||||
variant="border"
|
||||
>
|
||||
Runs
|
||||
</Button>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: enabled ? 'Disable' : 'Enable',
|
||||
icon: enabled ? faToggleOff : faToggleOn,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
setScheduleEnabled(path, enabled ? false : true)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
type: 'delete',
|
||||
icon: faTrash,
|
||||
disabled: !canWrite,
|
||||
action: async () => {
|
||||
await ScheduleService.deleteSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path
|
||||
})
|
||||
loadSchedules()
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
scheduleEditor?.openEdit(path, is_flow)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'View Runs',
|
||||
icon: faList,
|
||||
href: '/runs/' + path
|
||||
},
|
||||
{
|
||||
displayName: 'Run now',
|
||||
icon: faPlay,
|
||||
action: () => {
|
||||
runScheduleNow(script_path, args, is_flow)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Share' : 'See Permissions',
|
||||
icon: faShare,
|
||||
action: () => {
|
||||
shareModal.openDrawer(path, 'schedule')
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<div class="text-gray-600 text-xs truncate text-left font-light">
|
||||
schedule: {path}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</a>
|
||||
|
||||
<div class="w-10">
|
||||
{#if error}
|
||||
<Popover notClickable>
|
||||
<span class="flex h-4 w-4">
|
||||
<Icon
|
||||
class="text-red-600 animate-ping absolute inline-flex "
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during last job scheduling"
|
||||
/>
|
||||
<Icon
|
||||
class="text-red-600 relative inline-flex"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during last job scheduling"
|
||||
/>
|
||||
</span>
|
||||
<div slot="text">
|
||||
The schedule disabled itself because there was an error scheduling the next
|
||||
job: {error}
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="gap-2 items-center hidden md:flex">
|
||||
<Badge large color="blue">{schedule}</Badge>
|
||||
<Badge small color="gray">{timezone}</Badge>
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:flex flex-row gap-1 items-center">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
checked={enabled}
|
||||
on:change={(e) => {
|
||||
if (canWrite) {
|
||||
setScheduleEnabled(path, e.detail)
|
||||
} else {
|
||||
sendUserToast('not enough permission', true)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div class="flex gap-2 items-center justify-end">
|
||||
<Button
|
||||
href={`/runs/?schedule_path=${path}`}
|
||||
size="xs"
|
||||
startIcon={{ icon: faList }}
|
||||
color="light"
|
||||
variant="border"
|
||||
>
|
||||
Runs
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openEdit(path, is_flow)}
|
||||
size="xs"
|
||||
startIcon={{ icon: faPen }}
|
||||
color="dark"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: `View ${is_flow ? 'Flow' : 'Script'}`,
|
||||
icon: faEye,
|
||||
action: () => {
|
||||
goto(href)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
type: 'delete',
|
||||
icon: faTrash,
|
||||
disabled: !canWrite,
|
||||
action: async () => {
|
||||
await ScheduleService.deleteSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path
|
||||
})
|
||||
loadSchedules()
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
scheduleEditor?.openEdit(path, is_flow)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'View Runs',
|
||||
icon: faList,
|
||||
href: '/runs/?schedule_path=' + path
|
||||
},
|
||||
{
|
||||
displayName: 'Run now',
|
||||
icon: faPlay,
|
||||
action: () => {
|
||||
runScheduleNow(script_path, args, is_flow)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Share' : 'See Permissions',
|
||||
icon: faShare,
|
||||
action: () => {
|
||||
shareModal.openDrawer(path, 'schedule')
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex justify-between items-baseline">
|
||||
<div class="flex gap-1.5 ml-0.5 items-baseline">
|
||||
{#each jobs ?? [] as job}
|
||||
{@const h = (avg_s ? job.duration_ms / avg_s : 1) * 10}
|
||||
<a href="/run/{job.id}">
|
||||
<JobPreview id={job.id}>
|
||||
<div>
|
||||
<div
|
||||
class="{job.success ? 'bg-green-300' : 'bg-red-300'} mx-auto w-1.5"
|
||||
style="height: {h}px"
|
||||
/>
|
||||
<!-- <div class="text-[0.6em] mt-0.5 text-center text-gray-500"
|
||||
>{(job.duration_ms / 1000).toFixed(2)}s</div
|
||||
> -->
|
||||
</div>
|
||||
</JobPreview>
|
||||
</a>
|
||||
{/each}
|
||||
{#if avg_s}
|
||||
<div class="pl-2 text-gray-600 text-2xs">Avg: {(avg_s / 1000).toFixed(2)}s</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-wrap text-[0.7em] text-gray-500 gap-1 justify-end truncate pr-2"
|
||||
><div class="truncate">edited by {edited_by}</div><div class="truncate"
|
||||
>the {displayDate(edited_at)}</div
|
||||
></div
|
||||
></div
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<NoItemFound />
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
|
||||
Reference in New Issue
Block a user