improve schedule section of run page

This commit is contained in:
Ruben Fiszel
2023-07-09 22:35:33 +02:00
parent acbe2d4e05
commit 91d4664aaa
12 changed files with 372 additions and 276 deletions
+138 -134
View File
@@ -1230,6 +1230,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun"
]
},
@@ -2116,6 +2117,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun"
]
},
@@ -2792,6 +2794,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun"
]
},
@@ -3902,6 +3905,7 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun"
]
},
@@ -4019,106 +4023,6 @@
},
"query": "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND email = $2)"
},
"8caa01546506f42740b7973a3a45e40093a06714b8524570c5143b77af4a8e19": {
"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"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true,
false,
true
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
}
},
"query": "SELECT * FROM schedule WHERE workspace_id = $1 ORDER BY edited_at desc LIMIT $2 OFFSET $3"
},
"8cc4fa257cb8053d27eb7a555d6e1b993e4a74b063eac632df8da24f6b87130e": {
"describe": {
"columns": [
@@ -5544,7 +5448,39 @@
},
"query": "DELETE FROM resource WHERE path = $1 AND workspace_id = $2"
},
"bfc760c2f3f1477b177a7c570ddadda818dd3f57142165f381c1db01c26cf1bf": {
"c0635f65d561c1a6b183b8fea45320482375d0bb5e617a9dd236a631a7b9ff05": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "UPDATE workspace_settings SET error_handler = NULL WHERE workspace_id = $1"
},
"c07c9276945663d062cf0ff5b3323be681a0e2cb07a457ea9aede2daeff551cc": {
"describe": {
"columns": [
{
"name": "customer_id",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
true
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT customer_id FROM workspace_settings WHERE workspace_id = $1"
},
"c10348d26e3e3dc9e345d9044db35db9906617eb9c98aaf58a55d9681ffb85e3": {
"describe": {
"columns": [
{
@@ -5642,39 +5578,7 @@
]
}
},
"query": "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *"
},
"c0635f65d561c1a6b183b8fea45320482375d0bb5e617a9dd236a631a7b9ff05": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "UPDATE workspace_settings SET error_handler = NULL WHERE workspace_id = $1"
},
"c07c9276945663d062cf0ff5b3323be681a0e2cb07a457ea9aede2daeff551cc": {
"describe": {
"columns": [
{
"name": "customer_id",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
true
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT customer_id FROM workspace_settings WHERE workspace_id = $1"
"query": "UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS false RETURNING *"
},
"c1149f74ef0cca4a48be80f523d339d78e8e20e2bfacac9ad1b6b75c5ae370fb": {
"describe": {
@@ -6460,6 +6364,106 @@
},
"query": "UPDATE app SET versions = array_append(versions, $1) WHERE path = $2 AND workspace_id = $3"
},
"dd74fa9468b5fe8c8ad657ded06076c11a78d0206af2af20685c691d4d9520bb": {
"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"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true,
false,
true
],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
}
},
"query": "UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *"
},
"dd7940ec390357b268d616e1880516ecc08d506db2109efcce840f096d7a594e": {
"describe": {
"columns": [
+9
View File
@@ -4439,6 +4439,15 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- name: path
description: filter by path
in: query
schema:
type: string
- name: is_flow
in: query
schema:
type: boolean
responses:
"200":
description: schedule list
+22 -10
View File
@@ -17,6 +17,7 @@ use axum::{
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sql_builder::{prelude::Bind, SqlBuilder};
use sqlx::{Postgres, Transaction};
use std::str::FromStr;
use windmill_audit::{audit_log, ActionKind};
@@ -211,6 +212,14 @@ async fn edit_schedule(
Ok(path.to_string())
}
#[derive(Deserialize)]
pub struct ListScheduleQuery {
pub page: Option<usize>,
pub per_page: Option<usize>,
pub path: Option<String>,
pub is_flow: Option<bool>,
}
async fn list_schedule(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -218,16 +227,19 @@ async fn list_schedule(
Query(pagination): Query<Pagination>,
) -> JsonResult<Vec<Schedule>> {
let mut tx = user_db.begin(&authed).await?;
let (per_page, offset) = paginate(pagination);
let rows = sqlx::query_as!(
Schedule,
"SELECT * FROM schedule WHERE workspace_id = $1 ORDER BY edited_at desc LIMIT $2 OFFSET $3",
w_id,
per_page as i64,
offset as i64
)
.fetch_all(&mut tx)
.await?;
let (per_page, offset) =
paginate(Pagination { per_page: pagination.per_page, page: pagination.page });
let sqlb = SqlBuilder::select_from("schedule")
.field("*")
.order_by("edited_at", true)
.and_where("workspace_id = ?".bind(&w_id))
.offset(offset)
.limit(per_page)
.clone();
let sql = sqlb.sql().map_err(|e| Error::InternalErr(e.to_string()))?;
let rows = sqlx::query_as::<_, Schedule>(&sql)
.fetch_all(&mut tx)
.await?;
tx.commit().await?;
Ok(Json(rows))
}
+15 -2
View File
@@ -399,9 +399,9 @@ async fn create_script(
.execute(&mut tx)
.await?;
let schedulables = sqlx::query_as!(
let mut schedulables = sqlx::query_as!(
Schedule,
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *",
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND path != $2 AND workspace_id = $3 AND is_flow IS false RETURNING *",
ns.path,
p_path,
w_id,
@@ -409,6 +409,19 @@ async fn create_script(
.fetch_all(&mut tx)
.await?;
let schedule = sqlx::query_as!(Schedule,
"UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *",
ns.path,
p_path,
w_id,
)
.fetch_optional(&mut tx)
.await?;
if let Some(schedule) = schedule {
schedulables.push(schedule);
}
for schedule in schedulables {
clear_schedule(tx.transaction_mut(), &schedule.path, false).await?;
+1 -1
View File
@@ -210,6 +210,7 @@
function initPath() {
if (path != undefined && path != '') {
meta = pathToMeta(path)
onMetaChange()
return
}
if (initialPath == undefined || initialPath == '') {
@@ -287,7 +288,6 @@
const kind = e.detail
if (meta) {
if (kind === 'folder') {
console.log($userStore?.folders)
meta.owner = folders?.[0]?.name ?? ''
} else if (kind === 'group') {
meta.owner = 'all'
+1 -1
View File
@@ -152,7 +152,7 @@
{#if schedulable}
<div class="mt-10" />
<div class="flex gap-2 items-start flex-wrap justify-between mt-2 md:mt-6 mb-6">
<div class="flex-row-reverse flex w-full gap-4">
<div class="flex-row-reverse flex-wrap flex w-full gap-4">
<Button
{loading}
color="dark"
@@ -0,0 +1,174 @@
<script lang="ts">
import { faCalendar } from '@fortawesome/free-solid-svg-icons'
import ScheduleEditor from './ScheduleEditor.svelte'
import { Badge, Button } from './common'
import { workspaceStore } from '$lib/stores'
import { ScheduleService, type Schedule } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import Toggle from './Toggle.svelte'
import { ListOrdered, PenBox } from 'lucide-svelte'
import JobArgs from './JobArgs.svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Tooltip from './Tooltip.svelte'
export let isFlow: boolean
export let path: string
export let can_write: boolean
let scheduleEditor: ScheduleEditor
let schedule: Schedule | undefined = undefined
let schedules: Schedule[] | undefined = undefined
$: path && loadSchedule()
$: path && loadSchedules()
async function loadSchedule() {
try {
let exists = await ScheduleService.existsSchedule({
workspace: $workspaceStore ?? '',
path
})
if (exists) {
schedule = await ScheduleService.getSchedule({
workspace: $workspaceStore ?? '',
path
})
}
} catch (e) {
console.log('no primary schedule')
}
}
async function loadSchedules() {
try {
schedules = (
await ScheduleService.listSchedules({
workspace: $workspaceStore ?? '',
path,
isFlow
})
).filter((s) => s.path != path)
} catch (e) {
console.error('impossible to load schedules')
}
}
async function setScheduleEnabled(path: string, enabled: boolean): Promise<void> {
try {
await ScheduleService.setScheduleEnabled({
path,
workspace: $workspaceStore!,
requestBody: { enabled }
})
loadSchedule()
sendUserToast(`Schedule ${enabled ? 'enabled' : 'disabled'}`)
} catch (err) {
sendUserToast(`Cannot ` + (enabled ? 'disable' : 'enable') + ` schedule: ${err}`, true)
loadSchedule()
}
}
</script>
<ScheduleEditor
on:update={() => {
loadSchedule()
loadSchedules()
}}
bind:this={scheduleEditor}
/>
<div class="p-2 flex flex-col">
<Button
on:click={() => scheduleEditor?.openNew(isFlow, path)}
variant="border"
color="light"
size="xs"
startIcon={{ icon: faCalendar }}
>
New Schedule
</Button>
</div>
{#if schedule}
<div class="p-2 flex flex-col gap-2">
<div class="flex flex-row justify-between h-8">
<div class="flex flex-row gap-2">
<input
class="inline-block !w-32"
type="text"
id="cron-schedule"
name="cron-schedule"
placeholder="*/30 * * * *"
value={schedule.schedule}
disabled={true}
/>
<Badge color="indigo" small
>Primary schedule&nbsp;<Tooltip
>Share the same path as the script or flow it is attached to and its path get renamed
whenever the source path is renamed</Tooltip
></Badge
>
</div>
<div class="flex flex-row gap-2">
<Toggle
checked={schedule.enabled}
on:change={(e) => {
if (can_write) {
setScheduleEnabled(path, e.detail)
} else {
sendUserToast('not enough permission', true)
}
}}
options={{
right: 'On'
}}
size="xs"
/>
<Button size="xs" variant="border" color="light" href={`/runs/${path}`}>
<div class="flex flex-row gap-2">
<ListOrdered size={14} />
Runs
</div>
</Button>
<Button
size="xs"
color="dark"
on:click={() => scheduleEditor?.openEdit(path ?? '', isFlow)}
>
<PenBox size={14} />
</Button>
</div>
</div>
{#if Object.keys(schedule?.args ?? {}).length > 0}
<div class="">
<JobArgs args={schedule.args ?? {}} />
</div>
{:else}
<div class="text-xs texg-gray-700"> No arguments </div>
{/if}
</div>
{:else}
<Skeleton layout={[[8, 2]]} />
{/if}
<h3 class="px-2 pt-4">Other schedules</h3>
{#if schedules}
{#if schedules.length == 0}
<div class="text-xs text-gray-700 px-2"> No other schedules </div>
{:else}
<div class="flex flex-col divide-y px-2 pt-2">
{#each schedules as schedule}
<div class="grid grid-cols-6 text-2xs items-center py-2"
><div class="col-span-3 truncate">{schedule.path}</div><div>{schedule.schedule}</div>
<div>{schedule.enabled ? 'on' : 'off'}</div>
<button on:click={() => scheduleEditor?.openEdit(path, isFlow)}>edit</button>
</div>
{/each}
</div>
{/if}
{:else}
<Skeleton layout={[[8, 2]]} />
{/if}
@@ -251,8 +251,6 @@
/>
{/if}
<div class="mt-6">
<span class="font-semibold text-gray-700">Arguments</span>
{#if runnable}
{#if runnable?.schema && runnable.schema.properties && Object.keys(runnable.schema.properties).length > 0}
<SchemaForm disabled={!can_write} schema={runnable.schema} bind:isValid bind:args />
@@ -27,7 +27,7 @@
<div class="border-b p-2 shadow-md">
<div class="mx-auto">
<div class="flex w-full flex-wrap md:flex-nowrap justify-end gap-x-2 gap-y-4 h-8 items-center">
<div class="grow text-lg font-bold truncate">
<div class="grow text-lg font-bold truncate px-2">
{title}
</div>
<div class="flex gap-2 items-center">
@@ -46,7 +46,7 @@
<Tab value="schedule">
<span class="flex flex-row gap-2 items-center">
<CalendarCheck2 size={14} />
Schedule
Schedules
</span>
</Tab>
<Tab value="cli">
@@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/stores'
import { FlowService, JobService, ScheduleService, type Flow, type Schedule } from '$lib/gen'
import { FlowService, JobService, type Flow } from '$lib/gen'
import {
canWrite,
defaultIfEmptyString,
@@ -8,36 +8,32 @@
emptyString,
encodeState
} from '$lib/utils'
import { faCalendar, faCodeFork, faEdit } from '@fortawesome/free-solid-svg-icons'
import { faCodeFork, faEdit } from '@fortawesome/free-solid-svg-icons'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import DetailPageLayout from '$lib/components/details/DetailPageLayout.svelte'
import { goto } from '$app/navigation'
import { Alert, Button, Skeleton } from '$lib/components/common'
import JobArgs from '$lib/components/JobArgs.svelte'
import { Alert, Skeleton } from '$lib/components/common'
import MoveDrawer from '$lib/components/MoveDrawer.svelte'
import RunForm from '$lib/components/RunForm.svelte'
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
import ShareModal from '$lib/components/ShareModal.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import Urlize from '$lib/components/Urlize.svelte'
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
import SavedInputs from '$lib/components/SavedInputs.svelte'
import { FolderOpen, Archive, Trash, Server, Share, PenBox, ListOrdered } from 'lucide-svelte'
import { FolderOpen, Archive, Trash, Server, Share } from 'lucide-svelte'
import DetailPageHeader from '$lib/components/details/DetailPageHeader.svelte'
import WebhooksPanel from '$lib/components/details/WebhooksPanel.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import CliHelpBox from '$lib/components/CliHelpBox.svelte'
import InlineCodeCopy from '$lib/components/InlineCodeCopy.svelte'
import FlowGraphViewer from '$lib/components/FlowGraphViewer.svelte'
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
import RunPageSchedules from '$lib/components/RunPageSchedules.svelte'
let flow: Flow | undefined
let schedule: Schedule | undefined
let can_write = false
let path = $page.params.path
let shareModal: ShareModal
@@ -47,24 +43,6 @@
$: {
if ($workspaceStore && $userStore) {
loadFlow()
loadSchedule()
}
}
async function loadSchedule() {
try {
let exists = await ScheduleService.existsSchedule({
workspace: $workspaceStore ?? '',
path
})
if (exists) {
schedule = await ScheduleService.getSchedule({
workspace: $workspaceStore ?? '',
path
})
}
} catch (e) {
console.log('no primary schedule')
}
}
@@ -83,22 +61,6 @@
goto('/')
}
async function setScheduleEnabled(path: string, enabled: boolean): Promise<void> {
try {
await ScheduleService.setScheduleEnabled({
path,
workspace: $workspaceStore!,
requestBody: { enabled }
})
loadSchedule()
sendUserToast(`Schedule ${enabled ? 'enabled' : 'disabled'}`)
} catch (err) {
sendUserToast(`Cannot ` + (enabled ? 'disable' : 'enable') + ` schedule: ${err}`, true)
loadSchedule()
}
}
async function loadFlow(): Promise<void> {
flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path })
can_write = canWrite(flow.path, flow.extra_perms!, $userStore)
@@ -126,7 +88,6 @@
})
await goto('/run/' + run + '?workspace=' + $workspaceStore)
}
let scheduleEditor: ScheduleEditor
let args = undefined
@@ -229,7 +190,6 @@
layout={[0.75, [2, 0, 2], 2.25, [{ h: 1.5, w: 40 }], 0.2, [{ h: 1, w: 30 }]]}
/>
<svelte:window on:keydown={onKeyDown} />
<ScheduleEditor on:update={() => loadSchedule()} bind:this={scheduleEditor} />
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
<ShareModal bind:this={shareModal} />
<MoveDrawer
@@ -237,7 +197,6 @@
on:update={async (e) => {
await goto('/flows/get/' + e.detail + `?workspace=${$workspaceStore}`)
loadFlow()
loadSchedule()
}}
/>
@@ -326,71 +285,7 @@
/>
</svelte:fragment>
<svelte:fragment slot="schedule">
<div class="flex flex-row justify-end p-2">
<Button
on:click={() => scheduleEditor?.openNew(true, flow?.path ?? '')}
variant="border"
color="light"
size="xs"
startIcon={{ icon: faCalendar }}
>
New Schedule
</Button>
</div>
{#if schedule}
<div class="p-2 flex flex-col gap-2">
<div class="flex flex-row justify-between h-8">
<div class="flex flex-row gap-2">
<input
class="inline-block !w-32"
type="text"
id="cron-schedule"
name="cron-schedule"
placeholder="*/30 * * * *"
value={schedule.schedule}
disabled={true}
/>
<Badge color="indigo" small>Primary schedule</Badge>
</div>
<div class="flex flex-row gap-2">
<Toggle
checked={schedule.enabled}
on:change={(e) => {
if (can_write) {
setScheduleEnabled(path, e.detail)
} else {
sendUserToast('not enough permission', true)
}
}}
options={{
right: 'On'
}}
size="xs"
/>
<Button size="xs" variant="border" color="light" href={`/runs/${flow?.path}`}>
<div class="flex flex-row gap-2">
<ListOrdered size={14} />
Runs
</div>
</Button>
<Button
size="xs"
color="dark"
on:click={() => scheduleEditor?.openEdit(flow?.path ?? '', true)}
>
<PenBox size={14} />
</Button>
</div>
</div>
{#if Object.keys(schedule?.args ?? {}).length > 0}
<div class="">
<JobArgs args={schedule.args ?? {}} />
</div>
{:else}
<div class="text-xs texg-gray-700"> This flow takes no argument </div>
{/if}
</div>
{/if}
<RunPageSchedules isFlow={true} path={flow.path ?? ''} {can_write} />
</svelte:fragment>
<svelte:fragment slot="cli">
<div class="p-2">
@@ -9,7 +9,7 @@
displayDaysAgo,
truncateHash
} from '$lib/utils'
import { faEdit, faCalendar, faCodeFork, faHistory } from '@fortawesome/free-solid-svg-icons'
import { faEdit, faCodeFork, faHistory } from '@fortawesome/free-solid-svg-icons'
import Tooltip from '$lib/components/Tooltip.svelte'
import ShareModal from '$lib/components/ShareModal.svelte'
import { userStore, workspaceStore } from '$lib/stores'
@@ -20,7 +20,6 @@
Tabs,
Tab,
TabContent,
Button,
Badge,
Alert,
DrawerContent,
@@ -56,6 +55,7 @@
import { scriptToHubUrl } from '$lib/hub'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import ScriptVersionHistory from '$lib/components/ScriptVersionHistory.svelte'
import RunPageSchedules from '$lib/components/RunPageSchedules.svelte'
let script: Script | undefined
let topHash: string | undefined
@@ -352,6 +352,7 @@
/>
<svelte:window on:keydown={onKeyDown} />
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
<ScheduleEditor bind:this={scheduleEditor} />
<ShareModal bind:this={shareModal} />
@@ -482,17 +483,7 @@
<WebhooksPanel scopes={[`run:script/${script?.path}`]} {webhooks} {args} />
</svelte:fragment>
<svelte:fragment slot="schedule">
<div class="p-2 flex flex-col">
<Button
on:click={() => scheduleEditor?.openNew(false, script?.path ?? '')}
variant="border"
color="light"
size="xs"
startIcon={{ icon: faCalendar }}
>
New Schedule
</Button>
</div>
<RunPageSchedules isFlow={false} path={script.path ?? ''} {can_write} />
</svelte:fragment>
<svelte:fragment slot="details">
<div>