diff --git a/frontend/src/lib/components/WorkerRepl.svelte b/frontend/src/lib/components/WorkerRepl.svelte index e49c8211a1..e6c79cca05 100644 --- a/frontend/src/lib/components/WorkerRepl.svelte +++ b/frontend/src/lib/components/WorkerRepl.svelte @@ -2,11 +2,11 @@ import { onMount } from 'svelte' import { Terminal } from 'xterm' import 'xterm/css/xterm.css' - import { runScriptAndPollResult } from './jobs/utils' + import { pollJobResult, runScript } from './jobs/utils' import { workspaceStore } from '$lib/stores' import { Badge, Button, Drawer, DrawerContent, Skeleton } from './common' import DarkModeObserver from './DarkModeObserver.svelte' - import { Library, Play } from 'lucide-svelte' + import { Eye, Library, Play, RefreshCw, Square } from 'lucide-svelte' import Editor from './Editor.svelte' import WorkspaceScriptPicker from './flows/pickers/WorkspaceScriptPicker.svelte' import ToggleHubWorkspace from './ToggleHubWorkspace.svelte' @@ -17,6 +17,10 @@ import { FitAddon } from '@xterm/addon-fit' import { Readline } from 'xterm-readline' import Tooltip from './Tooltip.svelte' + import { JobService, type QueuedJob } from '$lib/gen' + import { sendUserToast } from '$lib/toast' + import Select from './Select.svelte' + import { emptyString } from '$lib/utils' let container: HTMLDivElement let term: Terminal @@ -29,12 +33,17 @@ let editor = $state(null) let darkMode = $state(false) let pick_existing: 'workspace' | 'hub' = $state('workspace') + let jobId = $state('') + let selectedJobId = $state('') let codeViewer: Drawer | undefined = $state() let filter = $state('') let { tag }: Props = $props() let code: string = $state('') let working_directory = $state('~') let homeDirectory: string = '~' + let pendingsJobs: Array = $state([]) + let loadingPendingJobs = $state(false) + let isCancelingJob = $state(false) let prompt = $derived( `$-${working_directory === '/' ? '/' : working_directory.split('/').at(-1)} ` ) @@ -86,7 +95,7 @@ } } - let result: any = await runScriptAndPollResult({ + jobId = await runScript({ workspace: $workspaceStore!, requestBody: { language: 'bash', @@ -95,6 +104,9 @@ args: {} } }) + + let result: any = await pollJobResult(jobId, $workspaceStore!) + if (isOnlyCdCommand) { working_directory = (result as string).replace(/(\r\n|\n|\r)/g, '') result = '' @@ -109,6 +121,40 @@ const rl = new Readline() + async function listPendingJobs() { + try { + loadingPendingJobs = true + pendingsJobs = await JobService.listQueue({ + workspace: $workspaceStore!, + tag, + running: true + }) + } catch (error) { + sendUserToast(error.body || error.message) + } finally { + loadingPendingJobs = false + } + } + + async function listPendingJobsAndUpdateSelectedJobid() { + await listPendingJobs() + if (!pendingsJobs.find((pendingJob) => pendingJob.id === selectedJobId)) { + selectedJobId = '' + } + } + + async function cancelJob(jobId: string) { + try { + await JobService.cancelQueuedJob({ + workspace: $workspaceStore! ?? '', + id: jobId, + requestBody: {} + }) + } catch (err) { + sendUserToast(err.BodyDropPivotTarget, true) + } + } + onMount(async () => { term = new Terminal({ cursorBlink: true, @@ -133,6 +179,11 @@ setTimeout(readLine) } + rl.setCtrlCHandler(async () => { + await cancelJob(jobId) + rl.read(prompt).then(processLine) + }) + const fitAddon = new FitAddon() term.loadAddon(fitAddon) term.open(container) @@ -176,9 +227,8 @@ } async function onScriptPick(e: { detail: { path: string } }) { - codeObj = undefined - codeViewer?.openDrawer?.() codeObj = await getScriptByPath(e.detail.path ?? '') + codeViewer?.openDrawer?.() } async function replacePromptWithCommand(command: string) { @@ -192,6 +242,8 @@ await handleCommand(input) term.write(prompt) } + + listPendingJobs() @@ -221,8 +273,94 @@
-
-
+
+
+ {#if pendingsJobs.length === 0} + + {:else} +
+
-
-
+
+
diff --git a/frontend/src/lib/components/jobs/utils.ts b/frontend/src/lib/components/jobs/utils.ts index 6e5f570529..294df7c3ff 100644 --- a/frontend/src/lib/components/jobs/utils.ts +++ b/frontend/src/lib/components/jobs/utils.ts @@ -1,22 +1,57 @@ import { JobService, type RunScriptByPathData, type RunScriptPreviewData } from '$lib/gen' - -function isRunScriptByPathData(arg:RunScriptPreviewData | RunScriptByPathData): arg is RunScriptByPathData { - return (arg as RunScriptByPathData).path !== undefined; +function isRunScriptByPathData( + arg: RunScriptPreviewData | RunScriptByPathData +): arg is RunScriptByPathData { + return (arg as RunScriptByPathData).path !== undefined } -export async function runScriptAndPollResult( - data: RunScriptPreviewData | RunScriptByPathData, - { maxRetries = 7, withJobData }: { maxRetries?: number; withJobData?: boolean } = {} +type RunScriptOptions = { + maxRetries?: number + withJobData?: boolean +} + +/** + * @function runScript + * @param {RunScriptPreviewData | RunScriptByPathData} data - Data for running the script. + * @returns {Promise} A UUID representing the running script. + * + * @example + * const uuid = await runScript(data) + */ +export async function runScript(data: RunScriptPreviewData | RunScriptByPathData) { + const uuid = ( + isRunScriptByPathData(data) + ? await JobService.runScriptByPath(data) + : await JobService.runScriptPreview(data) + ) as string + + return uuid +} + +/** + * @function pollJobResult + * @description Polls a job result by UUID until success, failure, or max retries reached. + * @param {string} uuid - Job UUID. + * @param {string} workspace - Workspace identifier. + * @param {RunScriptOptions} [options] - Optional settings like retries and job data inclusion. + * @returns {Promise} Final job result or throws error if it fails. + * + * @example + * const result = await pollJobResult(uuid, 'my-workspace', { maxRetries: 5, withJobData: true }); + */ +export async function pollJobResult( + uuid: string, + workspace: string, + { maxRetries = 7, withJobData }: RunScriptOptions = {} ): Promise { - const uuid = (isRunScriptByPathData(data) ? await JobService.runScriptByPath(data) : await JobService.runScriptPreview(data)) as string let attempts = 0 while (attempts < maxRetries) { try { await new Promise((resolve) => setTimeout(resolve, 500 * (attempts || 0.75))) const job = await JobService.getCompletedJobResultMaybe({ id: uuid, - workspace: data.workspace + workspace }) if (job.success) { if (withJobData) { @@ -41,3 +76,12 @@ export async function runScriptAndPollResult( throw 'Could not get job result, should not get here' } + +export async function runScriptAndPollResult( + data: RunScriptPreviewData | RunScriptByPathData, + runScriptOptions?: RunScriptOptions +): Promise { + const uuid = await runScript(data) + + return await pollJobResult(uuid, data.workspace, runScriptOptions) +}