feat: add schedule settable from pull flows

This commit is contained in:
Ruben Fiszel
2022-07-27 12:18:02 +02:00
parent e85c60ffa3
commit caecbfd0d9
19 changed files with 343 additions and 161 deletions
+3
View File
@@ -3793,6 +3793,7 @@ components:
- offset_
- extra_perms
- is_flow
- enabled
NewSchedule:
type: object
@@ -3809,6 +3810,8 @@ components:
type: boolean
args:
$ref: "#/components/schemas/ScriptArgs"
enabled:
type: boolean
required:
- path
- schedule
+10 -3
View File
@@ -64,6 +64,7 @@ pub struct NewSchedule {
pub script_path: String,
pub is_flow: bool,
pub args: Option<serde_json::Value>,
pub enabled: Option<bool>,
}
pub async fn push_scheduled_job<'c>(
@@ -133,7 +134,7 @@ async fn create_schedule(
check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?;
let schedule = sqlx::query_as!(Schedule,
"INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *",
"INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *",
w_id,
ns.path,
ns.schedule,
@@ -141,7 +142,8 @@ async fn create_schedule(
&authed.username,
ns.script_path,
ns.is_flow,
ns.args
ns.args,
ns.enabled
)
.fetch_one(&mut tx)
.await?;
@@ -165,8 +167,13 @@ async fn create_schedule(
)
.await?;
let tx = push_scheduled_job(tx, schedule).await?;
let tx = if ns.enabled.unwrap_or(true) {
push_scheduled_job(tx, schedule).await?
} else {
tx
};
tx.commit().await?;
Ok(ns.path.to_string())
}
+1 -1
View File
@@ -296,7 +296,7 @@
{disabled}
class="default-button-secondary items-center leading-4 py-0 my-px px-1 float-right"
on:click={() => (value = undefined)}
>Reset<Tooltip>Reset to default value</Tooltip></button
>Reset&nbsp;<Tooltip>Reset to default value</Tooltip></button
>
</div>
{/if}
+9 -2
View File
@@ -12,6 +12,7 @@
let preview: string[] = []
let cronError = ''
export let schedule: string = '0 0 12 * *'
let limit = 3
$: handleScheduleInput(schedule)
@@ -69,11 +70,17 @@
<CollapseLink text="preview next runs" open={true}>
{#if preview && preview.length > 0}
<div class="text-sm text-gray-700 border p-2 rounded-md">
<div class="flex flex-row justify-between">The next 10 runs will be scheduled at:</div>
<div class="flex flex-row justify-between">The next runs will be scheduled at:</div>
<ul class="list-disc mx-12">
{#each preview as p}
{#each preview.slice(0, limit) as p}
<li class="mx-2 text-gray-700 text-sm">{displayDate(p)}</li>
{/each}
<li class="text-sm mx-2">...</li>
{#if limit != 10}
<button class="underline text-gray-400" on:click={() => (limit = 10)}>Load more</button>
{:else}
<button class="underline text-gray-400" on:click={() => (limit = 3)}>Load less</button>
{/if}
</ul>
</div>
{/if}
+1 -3
View File
@@ -252,9 +252,7 @@
(lastWsAttempt.getTime() - new Date().getTime() > 60000 && nbWsAttempt < 2)
) {
if (!websocketAlive.black && !websocketAlive.deno && !websocketAlive.pyright) {
sendUserToast(
'Smart assistant got disconnected. Reconnecting to windmill language server for smart assistance'
)
console.log('reconnecting to language servers')
lastWsAttempt = new Date()
nbWsAttempt++
reloadWebsocket()
+79 -18
View File
@@ -1,12 +1,13 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { FlowService, type Flow } from '$lib/gen'
import { FlowService, ScheduleService, ScriptService, type Flow } from '$lib/gen'
import { clearPreviewResults, hubScripts, workspaceStore } from '$lib/stores'
import { loadHubScripts, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
import { formatCron, loadHubScripts, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
import { onMount } from 'svelte'
import { OFFSET } from './CronInput.svelte'
import FlowEditor from './FlowEditor.svelte'
import { flowStore, type FlowMode } from './flows/flowStore'
import { flowStore, mode } from './flows/flowStore'
import { flowToMode } from './flows/utils'
import ScriptSchema from './ScriptSchema.svelte'
@@ -14,36 +15,90 @@
export let initialPath: string = ''
let pathError = ''
let mode: FlowMode
let scheduleArgs: Record<string, any>
let scheduleEnabled
let scheduleCron: string
$: step = Number($page.url.searchParams.get('step')) || 1
async function createSchedule(path: string) {
await ScheduleService.createSchedule({
workspace: $workspaceStore!,
requestBody: {
path: path,
schedule: formatCron(scheduleCron),
offset: OFFSET,
script_path: path,
is_flow: true,
args: scheduleArgs,
enabled: scheduleEnabled
}
})
}
async function saveFlow(): Promise<void> {
const newFlow = flowToMode($flowStore, mode)
const flow = flowToMode($flowStore, $mode)
if (initialPath === '') {
await FlowService.createFlow({
workspace: $workspaceStore!,
requestBody: {
path: newFlow.path,
summary: newFlow.summary,
description: newFlow.description ?? '',
value: newFlow.value,
schema: newFlow.schema
path: flow.path,
summary: flow.summary,
description: flow.description ?? '',
value: flow.value,
schema: flow.schema
}
})
if ($mode == 'pull') {
await createSchedule(flow.path)
}
} else {
await FlowService.updateFlow({
workspace: $workspaceStore!,
path: newFlow.path,
path: initialPath,
requestBody: {
path: newFlow.path,
summary: newFlow.summary,
description: newFlow.description ?? '',
value: newFlow.value,
schema: newFlow.schema
path: flow.path,
summary: flow.summary,
description: flow.description ?? '',
value: flow.value,
schema: flow.schema
}
})
const scheduleExists = await ScheduleService.existsSchedule({
workspace: $workspaceStore ?? '',
path: initialPath
})
if (scheduleExists) {
const schedule = await ScheduleService.getSchedule({
workspace: $workspaceStore ?? '',
path: initialPath
})
if (
schedule.path != flow.path ||
JSON.stringify(schedule.args) != JSON.stringify(scheduleArgs) ||
schedule.schedule != scheduleCron
) {
await ScheduleService.updateSchedule({
workspace: $workspaceStore ?? '',
path: initialPath,
requestBody: {
schedule: formatCron(scheduleCron),
script_path: flow.path,
is_flow: true,
args: scheduleArgs
}
})
}
if (scheduleEnabled != schedule.enabled) {
await ScheduleService.setScheduleEnabled({
workspace: $workspaceStore ?? '',
path: flow.path,
requestBody: { enabled: scheduleEnabled }
})
}
} else {
await createSchedule(flow.path)
}
}
sendUserToast(`Success! flow saved at ${$flowStore.path}`)
goto(`/flows/get/${$flowStore.path}`)
@@ -54,7 +109,7 @@
}
flowStore.subscribe((flow: Flow) => {
setQueryWithoutLoad($page.url, 'state', btoa(JSON.stringify(flowToMode(flow, mode))))
setQueryWithoutLoad($page.url, 'state', btoa(JSON.stringify(flowToMode(flow, $mode))))
})
onMount(() => {
@@ -121,7 +176,13 @@
<!-- metadata -->
{#if step === 1}
<FlowEditor bind:mode bind:pathError bind:initialPath />
<FlowEditor
bind:pathError
bind:initialPath
bind:scheduleEnabled
bind:scheduleCron
bind:scheduleArgs
/>
{:else if step === 2}
<ScriptSchema
synchronizedHeader={false}
+67 -29
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import { FlowModuleValue } from '$lib/gen'
import { ScheduleService } from '$lib/gen'
import { faFileExport, faFileImport, faGlobe, faPlus } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import FlowPreview from './FlowPreview.svelte'
import CopyFirstStepSchema from './flows/CopyFirstStepSchema.svelte'
import { addModule, flowStore, initFlow, type FlowMode } from './flows/flowStore'
import { addModule, flowStore, initFlow, mode } from './flows/flowStore'
import ModuleStep from './ModuleStep.svelte'
import Path from './Path.svelte'
import RadioButtonV2 from './RadioButtonV2.svelte'
@@ -20,21 +20,41 @@
import CronInput from './CronInput.svelte'
import CollapseLink from './CollapseLink.svelte'
import Toggle from './Toggle.svelte'
import { loadSchema } from '$lib/scripts'
import SchemaForm from './SchemaForm.svelte'
import Tooltip from './Tooltip.svelte'
import { workspaceStore } from '$lib/stores'
export let pathError = ''
export let initialPath: string = ''
export let mode: FlowMode =
$flowStore?.value.modules[1]?.value.type == FlowModuleValue.type.FORLOOPFLOW ? 'pull' : 'push'
let allowSchedule = false
let cronSchedule: string | undefined
export let scheduleArgs: Record<string, any> = {}
export let scheduleEnabled = false
export let scheduleCron: string = '0 */5 * * *'
let jsonSetter: Modal
let jsonViewer: Modal
let jsonValue: string = ''
async function loadSchedule() {
try {
const schedule = await ScheduleService.getSchedule({
workspace: $workspaceStore ?? '',
path: initialPath
})
scheduleEnabled = schedule.enabled
scheduleCron = schedule.schedule
scheduleArgs = scheduleArgs
console.log(schedule.enabled, schedule.schedule)
} catch (e) {
console.log(`no primary schedule found for ${initialPath}`)
}
}
$: if ($workspaceStore && initialPath != '') {
loadSchedule()
}
let open = 0
let args: Record<string, any> = {}
</script>
@@ -63,7 +83,7 @@
<Modal bind:this={jsonViewer}>
<div slot="title">See JSON</div>
<div slot="content" class="h-full">
<FlowViewer flow={flowToMode($flowStore, mode)} tab="json" />
<FlowViewer flow={flowToMode($flowStore, $mode)} tab="json" />
</div>
</Modal>
@@ -106,7 +126,7 @@
}
url.searchParams.append(
'flow',
btoa(JSON.stringify(flowToMode(openFlow, mode)))
btoa(JSON.stringify(flowToMode(openFlow, $mode)))
)
window.open(url, '_blank')?.focus()
}
@@ -154,35 +174,48 @@
options={[
[
{
title: 'Push',
title: 'UI or webhook triggered',
desc: 'Trigger this flow through the generated UI, a manual schedule or by calling the associated webhook'
},
'push'
],
[
{
title: 'Pull',
title: 'Watching changes regularly',
desc: 'The first module of this flow is a trigger script whose purpose is to pull data from an external source and return all new items since last run. This flow is meant to be scheduled very regularly to reduce latency to react to new events. It will trigger the rest of the flow once per item. If no new items, the flow will be skipped.'
},
'pull'
]
]}
bind:value={mode}
bind:value={$mode}
/>
<div class="p-4 hidden">
<CollapseLink text="set primary schedule" open={mode == 'pull'}>
<Toggle
bind:value={allowSchedule}
options={{
left: { label: 'disabled', value: false },
right: { label: 'enabled', value: true }
}}
/>
<div class="p-2 mt-2 rounded" class:bg-gray-300={!allowSchedule}>
<CronInput schedule={cronSchedule} />
</div>
</CollapseLink>
</div>
{#if $mode == 'pull'}
<div class="p-4">
<CollapseLink text="set primary schedule" open={true}>
<Tooltip
>The primary schedule of a flow is simply a schedule that has the same name as a
flow. It can be set and enabled directly within the flow editor. "Watching for
new changes" flows are meant to be watching regularly for new items in an
external systems. The primary schedule purpose is there to set the periodicity
at which you want this watcher to operate.
</Tooltip> &nbsp;
<Toggle
bind:checked={scheduleEnabled}
options={{
left: { label: 'disabled', value: false },
right: { label: 'enabled', value: true }
}}
/>
<div class="p-2 mt-2 rounded" class:bg-gray-300={!scheduleEnabled}>
{#if !scheduleEnabled}
<span class="font-black">No next scheduled run when disabled</span>
{/if}
<CronInput bind:schedule={scheduleCron} />
</div>
<SchemaForm schema={$flowStore.schema} bind:args={scheduleArgs} />
</CollapseLink>
</div>
{/if}
</div>
</li>
<li class="flex flex-row flex-shrink max-w-full mx-auto mt-20">
@@ -194,10 +227,15 @@
<CopyFirstStepSchema />
</div>
<div class="p-4">
<SchemaEditor schema={$flowStore.schema} />
<SchemaEditor
on:change={() => {
$flowStore = $flowStore
}}
schema={$flowStore.schema}
/>
<div class="my-4" />
<FlowPreview
{mode}
mode={$mode}
flow={$flowStore}
i={$flowStore?.value.modules.length}
bind:args
@@ -219,7 +257,7 @@
</button>
</div>
</li>
<ModuleStep bind:open bind:mod bind:args {i} {mode} />
<ModuleStep bind:open bind:mod bind:args {i} mode={$mode} />
{/each}
<li class="relative m-20 ">
<div class="relative flex justify-center">
@@ -20,7 +20,7 @@
class="flex flex-col default-secondary-button-v2 mb-2 grow"
class:selected={value == val}
>
<h2 class="mb-2 whitespace-nowrap">{label.title} <Tooltip>{label.desc}</Tooltip></h2>
<h2 class="mb-2">{label.title} <Tooltip>{label.desc}</Tooltip></h2>
</button>
{/each}
</div>
@@ -6,6 +6,9 @@
import { emptySchema, sendUserToast } from '$lib/utils'
import Tooltip from './Tooltip.svelte'
import TableCustom from './TableCustom.svelte'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let schema: Schema = emptySchema()
@@ -63,6 +66,7 @@
}
schema = schema
schemaString = JSON.stringify(schema, null, '\t')
dispatch('change', schema)
}
function startEditArgument(argName: string): void {
@@ -87,6 +91,7 @@
delete schema.properties[argName]
schema = schema
schemaString = JSON.stringify(schema, null, '\t')
dispatch('change', schema)
} else {
throw Error('Argument not found!')
}
@@ -46,10 +46,12 @@
}
</script>
<span class="mr-1">
{#if kind == 'read'}
<Badge tooltip={reason}>shared to you (read-only)</Badge>
{:else if kind == 'write'}
<Badge tooltip={reason}>shared to you</Badge>
{/if}
</span>
{#if kind == 'read' || kind == 'write'}
<span class="mr-1">
{#if kind == 'read'}
<Badge tooltip={reason}>shared to you (read-only)</Badge>
{:else if kind == 'write'}
<Badge tooltip={reason}>shared to you</Badge>
{/if}
</span>
{/if}
@@ -7,10 +7,14 @@ import { createInlineScriptModuleFromPath, getFirstStepSchema, loadSchemaFromMod
export type FlowMode = 'push' | 'pull'
export const mode = writable<FlowMode>('push')
export const flowStore = writable<Flow>(undefined)
export const schemasStore = writable<Schema[]>([])
export function initFlow(flow: Flow) {
const newMode = flow.value.modules[1]?.value.type === FlowModuleValue.type.FORLOOPFLOW ? 'pull' : 'push'
mode.set(newMode)
flow = flattenForloopFlows(flow, newMode)
schemasStore.set([])
flowStore.set(flow)
// For each module in flow, we should load the corresponding schema
@@ -19,6 +23,16 @@ export function initFlow(flow: Flow) {
})
}
export function flattenForloopFlows(flow: Flow, mode: FlowMode): Flow {
let newFlow: Flow = JSON.parse(JSON.stringify(flow))
if (mode == 'pull') {
const oldModules = newFlow.value.modules[1].value.value?.modules ?? []
newFlow.value.modules = newFlow.value.modules.slice(0, 1)
newFlow.value.modules.push(...oldModules)
}
return newFlow
}
export const isCopyFirstStepSchemaDisabled = derived(flowStore, (flow: Flow | undefined) => {
if (flow) {
const modules = flow.value.modules
@@ -35,15 +35,6 @@ export function flowToMode(flow: Flow | any, mode: FlowMode): Flow {
return flow
}
export function flattenForloopFlows(flow: Flow): Flow {
let newFlow: Flow = JSON.parse(JSON.stringify(flow))
if (newFlow.value.modules[1]?.value.type == FlowModuleValue.type.FORLOOPFLOW) {
const oldModules = newFlow.value.modules[1].value.value?.modules ?? []
newFlow.value.modules = newFlow.value.modules.slice(0, 1)
newFlow.value.modules.push(...oldModules)
}
return newFlow
}
export function getTypeAsString(arg: any): string {
if (arg === null) {
@@ -7,6 +7,7 @@
export let level = 0
export let isLast = true
export let currentPath: string = ''
export let pureViewer = false
const collapsedSymbol = '...'
let keys: string | any[]
@@ -67,6 +68,7 @@
level={level + 1}
isLast={index === keys.length - 1}
currentPath={computeKey(key)}
{pureViewer}
on:select
/>
{:else}
@@ -75,7 +77,9 @@
<WarningMessage />
{:else}
<span> {JSON.stringify(json[key])}</span>
<button class="ml-2 default-button-secondary py-0"> Select </button>
{#if !pureViewer}
<button class="ml-2 default-button-secondary py-0"> Select </button>
{/if}
{/if}
</button>
{/if}
+4
View File
@@ -176,6 +176,10 @@ export function canWrite(
return false
}
export function defaultIfEmptyString(str: string | undefined, dflt: string): string {
return str == undefined || str == '' ? dflt : str
}
export function removeKeysWithEmptyValues(obj: any): any {
Object.keys(obj).forEach((key) => (obj[key] === undefined ? delete obj[key] : {}))
}
+2 -1
View File
@@ -10,7 +10,7 @@
import { page } from '$app/stores'
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
import { initFlow } from '$lib/components/flows/flowStore'
import { initFlow, mode } from '$lib/components/flows/flowStore'
import { FlowService, type Flow } from '$lib/gen'
import { emptySchema, sendUserToast } from '$lib/utils'
@@ -42,6 +42,7 @@
$page.url.searchParams.delete('hub')
sendUserToast(`Flow has been loaded from hub flow id ${hubId}.`)
}
$mode = 'push'
initFlow(flow)
}
@@ -7,13 +7,12 @@
</script>
<script lang="ts">
import { FlowService, type Flow } from '$lib/gen'
import { FlowModuleValue, FlowService, type Flow } from '$lib/gen'
import { page } from '$app/stores'
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
import { workspaceStore } from '$lib/stores'
import { emptySchema } from '$lib/utils'
import { flattenForloopFlows } from '$lib/components/flows/utils'
import { initFlow } from '$lib/components/flows/flowStore'
const initialState = $page.url.searchParams.get('state')
@@ -42,7 +41,6 @@
workspace: $workspaceStore!,
path: flow.path
})
flow = flattenForloopFlows(flow)
initialPath = flow.path
initFlow(flow)
}
+71 -28
View File
@@ -8,8 +8,8 @@
<script lang="ts">
import { page } from '$app/stores'
import { FlowService, type Flow } from '$lib/gen'
import { displayDaysAgo, canWrite } from '$lib/utils'
import { FlowService, ScheduleService, type Flow, type Schedule } from '$lib/gen'
import { displayDaysAgo, canWrite, sendUserToast, defaultIfEmptyString } from '$lib/utils'
import Icon from 'svelte-awesome'
import {
faPlay,
@@ -22,14 +22,17 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import ShareModal from '$lib/components/ShareModal.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import SvelteMarkdown from 'svelte-markdown'
import Dropdown from '$lib/components/Dropdown.svelte'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import FlowViewer from '$lib/components/FlowViewer.svelte'
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
let flow: Flow | undefined
let schedule: Schedule | undefined
let can_write = false
let path = $page.params.path
@@ -37,16 +40,41 @@
$: {
if ($workspaceStore && $userStore) {
loadFlow(path)
loadFlow()
loadSchedule()
}
}
async function archiveFlow(hash: string): Promise<void> {
await FlowService.archiveFlowByPath({ workspace: $workspaceStore!, path })
loadFlow(path)
async function loadSchedule() {
try {
schedule = await ScheduleService.getSchedule({
workspace: $workspaceStore ?? '',
path
})
} catch (e) {
console.log('no primary schedule')
}
}
async function loadFlow(hash: string): Promise<void> {
async function archiveFlow(): Promise<void> {
await FlowService.archiveFlowByPath({ workspace: $workspaceStore!, path })
loadFlow()
}
async function setScheduleEnabled(path: string, enabled: boolean): Promise<void> {
try {
await ScheduleService.setScheduleEnabled({
path,
workspace: $workspaceStore!,
requestBody: { enabled }
})
loadSchedule()
} catch (err) {
sendUserToast(`Cannot ` + enabled ? 'disable' : 'enable' + ` schedule: ${err}`, true)
}
}
async function loadFlow(): Promise<void> {
flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path })
can_write = canWrite(flow.path, flow.extra_perms!, $userStore)
}
@@ -87,7 +115,7 @@
icon: faArchive,
type: 'delete',
action: () => {
flow?.path && archiveFlow(flow.path)
flow?.path && archiveFlow()
},
disabled: flow.archived || !can_write
}
@@ -137,31 +165,46 @@
{#if flow === undefined}
<p>loading</p>
{:else}
<h2>{flow.summary}</h2>
<p>Edited at {displayDaysAgo(flow.edited_at ?? '')} by {flow.edited_by}</p>
<div class="prose">
<SvelteMarkdown source={defaultIfEmptyString(flow.description, 'No description')} />
</div>
{#if schedule}
<div>
<h2 class="text-gray-700 pb-1 mb-3 border-b">Primary Schedule</h2>
<div>
<h3 class="text-gray-700 ">Enabled</h3>
<Toggle
checked={schedule.enabled}
on:change={(e) => {
if (can_write) {
setScheduleEnabled(path, e.detail)
} else {
sendUserToast('not enough permission', true)
}
}}
/>
</div>
<div class:bg-gray-300={!schedule.enabled}>
<div>
<h3 class="text-gray-700 ">Schedule</h3>
{schedule.schedule}
</div>
<div>
<h3 class="text-gray-700 ">Args</h3>
<ObjectViewer json={schedule.args ?? {}} pureViewer={true} />
</div>
</div>
</div>
{/if}
{#if flow.archived}
<div class="bg-red-100 border-l-4 border-red-500 text-orange-700 p-4" role="alert">
<p class="font-bold">Archived</p>
<p>This version was archived</p>
</div>
{/if}
<div>
<h3 class="text-gray-700 ">Edited at</h3>
{displayDaysAgo(flow.edited_at ?? '')}
</div>
<div>
<h3 class="text-gray-700 ">Last editor</h3>
{flow.edited_by}
</div>
<div>
<h3 class="text-gray-700 ">Summary</h3>
{flow.summary}
</div>
<div>
<h3 class="text-gray-700 ">Description</h3>
<div class="prose mt-5">
<SvelteMarkdown source={flow.description ?? ''} />
</div>
</div>
<div>
<span>Webhook to run this flow:</span>
<Tooltip
@@ -176,7 +219,7 @@
></pre>
</div>
<div>
<h3 class="text-gray-700 pb-1 mb-3 border-b">Flow</h3>
<h2 class="text-gray-700 pb-1 mb-3 border-b">Flow</h2>
<FlowViewer {flow} />
</div>
{/if}
+15 -16
View File
@@ -27,6 +27,7 @@
import { userStore, workspaceStore } from '$lib/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import Icon from 'svelte-awesome'
import Toggle from '$lib/components/Toggle.svelte'
type ScheduleW = Schedule & { canWrite: boolean }
@@ -79,36 +80,31 @@
</tr>
<tbody slot="body">
{#each schedules as { path, edited_by, edited_at, schedule, offset_, enabled, script_path, is_flow, extra_perms, canWrite }}
<tr class={enabled ? '' : 'bg-gray-200'}>
<tr class={enabled ? '' : 'bg-gray-100'}>
<td
><a href="/schedule/add?edit={path}&isFlow={is_flow}" style="cursor: pointer;"
>{path}</a
>
<div>
<SharedBadge {canWrite} extraPerms={extra_perms} />
</div>
<SharedBadge {canWrite} extraPerms={extra_perms} />
</td>
<td
>{script_path}<span class="text-2xs text-gray-500 bg-gray-100 font-mono ml-2"
<td class="whitespace-nowrap"
><a href="{is_flow ? '/flows/get' : '/scripts/get'}/{script_path}">{script_path}</a
><span class="text-2xs text-gray-500 bg-gray-100 font-mono ml-2"
>{is_flow ? 'flow' : 'script'}</span
></td
>
<td>{schedule}</td>
<td
><a
id="toggle-{path}"
style="cursor: pointer;"
on:click={() => {
<td>
<Toggle
checked={enabled}
on:change={(e) => {
if (canWrite) {
setScheduleEnabled(path, enabled ? false : true)
setScheduleEnabled(path, e.detail)
} else {
sendUserToast('not enough permission', true)
}
}}
><span class="m-auto block text-center">
<Icon data={enabled ? faToggleOn : faToggleOff} scale={1.5} />
</span></a
></td
/></td
>
<td>{offset_ < 0 ? '+' : ''}{(offset_ / 60) * -1}</td>
<td class="text-2xs">By {edited_by} <br />at {displayDate(edited_at)}</td>
@@ -171,4 +167,7 @@
/>
<style>
td {
@apply px-2;
}
</style>
@@ -9,7 +9,13 @@
<script lang="ts">
import { page } from '$app/stores'
import { ScriptService, type Script } from '$lib/gen'
import { truncateHash, sendUserToast, displayDaysAgo, canWrite } from '$lib/utils'
import {
truncateHash,
sendUserToast,
displayDaysAgo,
canWrite,
defaultIfEmptyString
} from '$lib/utils'
import Icon from 'svelte-awesome'
import {
faPlay,
@@ -230,6 +236,13 @@
{#if script === undefined}
<p>loading</p>
{:else}
<h2>{script.summary}</h2>
<p>Edited at {displayDaysAgo(script.created_at ?? '')} by {script.created_by}</p>
<div class="prose">
<SvelteMarkdown source={defaultIfEmptyString(script.description, 'No description')} />
</div>
{#if script.lock_error_logs}
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">Error deploying this script</p>
@@ -259,24 +272,6 @@
</div>
{/if}
<div>
<h3 class="text-gray-700 ">Edited at</h3>
{displayDaysAgo(script.created_at ?? '')}
</div>
<div>
<h3 class="text-gray-700 ">Last editor</h3>
{script.created_by}
</div>
<div>
<h3 class="text-gray-700 ">Summary</h3>
{script.summary}
</div>
<div>
<h3 class="text-gray-700 ">Description</h3>
<div class="prose mt-5">
<SvelteMarkdown source={script.description ?? ''} />
</div>
</div>
<div>
<h3>
Current hash <Tooltip
@@ -288,32 +283,38 @@
<p class="text-gray-700">
<a href="/scripts/get/{script?.hash}">{script?.hash}</a>
</p>
<span>Webhook to run this script and get job's uuid as response:</span>
<Tooltip
>Send a POST http request with a token as bearer token and the args respecting the
corresponding jsonschema as payload. To create a permanent token, go to your user setting
by clicking your username on the top-left. For more info about openapi, see <a
href="https://docs.windmill.dev/openapi/run-script-by-hash">openapi doc</a
></Tooltip
>
<h3 class="whitespace-nowrap mt-2">
Webhook to run this script and get job's uuid as response
<Tooltip
>Send a POST http request with a token as bearer token and the args respecting the
corresponding jsonschema as payload. To create a permanent token, go to your user
setting by clicking your username on the top-left. For more info about openapi, see <a
href="https://docs.windmill.dev/openapi/run-script-by-hash">openapi doc</a
></Tooltip
>
</h3>
<pre><code
><a href="/api/w/{$workspaceStore}/jobs/run/h/{script?.hash}"
>By hash: <a href="/api/w/{$workspaceStore}/jobs/run/h/{script?.hash}"
>/api/w/{$workspaceStore}/jobs/run/h/{script?.hash}</a
></code
></pre>
<pre><code
><a href="/api/w/{$workspaceStore}/jobs/run/p/{script?.path}"
>By path: <a href="/api/w/{$workspaceStore}/jobs/run/p/{script?.path}"
>/api/w/{$workspaceStore}/jobs/run/p/{script?.path}</a
></code
></pre>
<span>Endpoint to run this script and get job's result as response:</span>
<Tooltip
>Send a POST http request with a token as bearer token and the args respecting the
corresponding jsonschema as payload. To create a permanent token, go to your user setting
by clicking your username on the top-left. For more info about openapi, see <a
href="https://docs.windmill.dev/openapi/run-script-by-hash">openapi doc</a
></Tooltip
>
<h3 class="whitespace-nowrap mt-2">
Endpoint to run this script and get job's result as response
<Tooltip
>Send a POST http request with a token as bearer token and the args respecting the
corresponding jsonschema as payload. To create a permanent token, go to your user
setting by clicking your username on the top-left. For more info about openapi, see <a
href="https://docs.windmill.dev/openapi/run-script-by-hash">openapi doc</a
></Tooltip
>
</h3>
<pre><code
><a href="/api/w/{$workspaceStore}/jobs/run_wait_result/p/{script?.path}"
>/api/w/{$workspaceStore}/jobs/run_wait_result/p/{script?.path}</a
@@ -321,8 +322,8 @@
></pre>
</div>
<div>
<h3 class="text-gray-700">
Previous versions of this hash<Tooltip
<h3>
Previous versions of this hash <Tooltip
>When you edit a script, a new hash is created and old versions are archived</Tooltip
>
</h3>
@@ -361,3 +362,9 @@
{/if}
</div>
</CenteredPage>
<style>
h3 {
@apply text-lg mb-2 mt-4 text-gray-600;
}
</style>