|
2 | 2 | import { onMount } from 'svelte'
|
3 | 3 | import { Terminal } from 'xterm'
|
4 | 4 | import 'xterm/css/xterm.css'
|
5 |
| - import { runScriptAndPollResult } from './jobs/utils' |
| 5 | + import { pollJobResult, runScript } from './jobs/utils' |
6 | 6 | import { workspaceStore } from '$lib/stores'
|
7 | 7 | import { Badge, Button, Drawer, DrawerContent, Skeleton } from './common'
|
8 | 8 | import DarkModeObserver from './DarkModeObserver.svelte'
|
9 |
| - import { Library, Play } from 'lucide-svelte' |
| 9 | + import { Eye, Library, Play, RefreshCw, Square } from 'lucide-svelte' |
10 | 10 | import Editor from './Editor.svelte'
|
11 | 11 | import WorkspaceScriptPicker from './flows/pickers/WorkspaceScriptPicker.svelte'
|
12 | 12 | import ToggleHubWorkspace from './ToggleHubWorkspace.svelte'
|
|
17 | 17 | import { FitAddon } from '@xterm/addon-fit'
|
18 | 18 | import { Readline } from 'xterm-readline'
|
19 | 19 | import Tooltip from './Tooltip.svelte'
|
| 20 | + import { JobService, type QueuedJob } from '$lib/gen' |
| 21 | + import { sendUserToast } from '$lib/toast' |
| 22 | + import Select from './Select.svelte' |
| 23 | + import { emptyString } from '$lib/utils' |
20 | 24 |
|
21 | 25 | let container: HTMLDivElement
|
22 | 26 | let term: Terminal
|
|
29 | 33 | let editor = $state<Editor | null>(null)
|
30 | 34 | let darkMode = $state(false)
|
31 | 35 | let pick_existing: 'workspace' | 'hub' = $state('workspace')
|
| 36 | + let jobId = $state('') |
| 37 | + let selectedJobId = $state('') |
32 | 38 | let codeViewer: Drawer | undefined = $state()
|
33 | 39 | let filter = $state('')
|
34 | 40 | let { tag }: Props = $props()
|
35 | 41 | let code: string = $state('')
|
36 | 42 | let working_directory = $state('~')
|
37 | 43 | let homeDirectory: string = '~'
|
| 44 | + let pendingsJobs: Array<QueuedJob> = $state([]) |
| 45 | + let loadingPendingJobs = $state(false) |
| 46 | + let isCancelingJob = $state(false) |
38 | 47 | let prompt = $derived(
|
39 | 48 | `$-${working_directory === '/' ? '/' : working_directory.split('/').at(-1)} `
|
40 | 49 | )
|
|
86 | 95 | }
|
87 | 96 | }
|
88 | 97 |
|
89 |
| - let result: any = await runScriptAndPollResult({ |
| 98 | + jobId = await runScript({ |
90 | 99 | workspace: $workspaceStore!,
|
91 | 100 | requestBody: {
|
92 | 101 | language: 'bash',
|
|
95 | 104 | args: {}
|
96 | 105 | }
|
97 | 106 | })
|
| 107 | +
|
| 108 | + let result: any = await pollJobResult(jobId, $workspaceStore!) |
| 109 | +
|
98 | 110 | if (isOnlyCdCommand) {
|
99 | 111 | working_directory = (result as string).replace(/(\r\n|\n|\r)/g, '')
|
100 | 112 | result = ''
|
|
109 | 121 |
|
110 | 122 | const rl = new Readline()
|
111 | 123 |
|
| 124 | + async function listPendingJobs() { |
| 125 | + try { |
| 126 | + loadingPendingJobs = true |
| 127 | + pendingsJobs = await JobService.listQueue({ |
| 128 | + workspace: $workspaceStore!, |
| 129 | + tag, |
| 130 | + running: true |
| 131 | + }) |
| 132 | + } catch (error) { |
| 133 | + sendUserToast(error.body || error.message) |
| 134 | + } finally { |
| 135 | + loadingPendingJobs = false |
| 136 | + } |
| 137 | + } |
| 138 | +
|
| 139 | + async function listPendingJobsAndUpdateSelectedJobid() { |
| 140 | + await listPendingJobs() |
| 141 | + if (!pendingsJobs.find((pendingJob) => pendingJob.id === selectedJobId)) { |
| 142 | + selectedJobId = '' |
| 143 | + } |
| 144 | + } |
| 145 | +
|
| 146 | + async function cancelJob(jobId: string) { |
| 147 | + try { |
| 148 | + await JobService.cancelQueuedJob({ |
| 149 | + workspace: $workspaceStore! ?? '', |
| 150 | + id: jobId, |
| 151 | + requestBody: {} |
| 152 | + }) |
| 153 | + } catch (err) { |
| 154 | + sendUserToast(err.BodyDropPivotTarget, true) |
| 155 | + } |
| 156 | + } |
| 157 | +
|
112 | 158 | onMount(async () => {
|
113 | 159 | term = new Terminal({
|
114 | 160 | cursorBlink: true,
|
|
133 | 179 | setTimeout(readLine)
|
134 | 180 | }
|
135 | 181 |
|
| 182 | + rl.setCtrlCHandler(async () => { |
| 183 | + await cancelJob(jobId) |
| 184 | + rl.read(prompt).then(processLine) |
| 185 | + }) |
| 186 | +
|
136 | 187 | const fitAddon = new FitAddon()
|
137 | 188 | term.loadAddon(fitAddon)
|
138 | 189 | term.open(container)
|
|
176 | 227 | }
|
177 | 228 |
|
178 | 229 | async function onScriptPick(e: { detail: { path: string } }) {
|
179 |
| - codeObj = undefined |
180 |
| - codeViewer?.openDrawer?.() |
181 | 230 | codeObj = await getScriptByPath(e.detail.path ?? '')
|
| 231 | + codeViewer?.openDrawer?.() |
182 | 232 | }
|
183 | 233 |
|
184 | 234 | async function replacePromptWithCommand(command: string) {
|
|
192 | 242 | await handleCommand(input)
|
193 | 243 | term.write(prompt)
|
194 | 244 | }
|
| 245 | +
|
| 246 | + listPendingJobs() |
195 | 247 | </script>
|
196 | 248 |
|
197 | 249 | <DarkModeObserver bind:darkMode />
|
|
221 | 273 | </Drawer>
|
222 | 274 |
|
223 | 275 | <div class="h-screen flex flex-col">
|
224 |
| - <div class="m-1"> |
225 |
| - <div class="flex flex-col"> |
| 276 | + <div class="m-1 flex flex-col gap-2"> |
| 277 | + <div class="flex flex-col gap-1"> |
| 278 | + {#if pendingsJobs.length === 0} |
| 279 | + <Button |
| 280 | + loading={loadingPendingJobs} |
| 281 | + variant="border" |
| 282 | + color="light" |
| 283 | + wrapperClasses="self-stretch" |
| 284 | + on:click={async () => { |
| 285 | + await listPendingJobs() |
| 286 | + if (pendingsJobs.length === 0) { |
| 287 | + sendUserToast('No pending ssh jobs found') |
| 288 | + } |
| 289 | + }} |
| 290 | + startIcon={{ icon: RefreshCw }} |
| 291 | + > |
| 292 | + Load pending ssh jobs</Button |
| 293 | + > |
| 294 | + {:else} |
| 295 | + <div class="flex gap-1"> |
| 296 | + <Select |
| 297 | + loading={loadingPendingJobs} |
| 298 | + class="grow shrink" |
| 299 | + bind:value={selectedJobId} |
| 300 | + items={pendingsJobs.map((pendingJob) => ({ value: pendingJob.id }))} |
| 301 | + placeholder="Choose a pending job id" |
| 302 | + clearable |
| 303 | + disablePortal |
| 304 | + /> |
| 305 | + <Button |
| 306 | + variant="border" |
| 307 | + color="light" |
| 308 | + disabled={emptyString(selectedJobId)} |
| 309 | + wrapperClasses="self-stretch" |
| 310 | + on:click={listPendingJobsAndUpdateSelectedJobid} |
| 311 | + startIcon={{ icon: RefreshCw }} |
| 312 | + iconOnly |
| 313 | + /> |
| 314 | + <Button |
| 315 | + color="light" |
| 316 | + size="xs" |
| 317 | + variant="border" |
| 318 | + disabled={emptyString(selectedJobId)} |
| 319 | + startIcon={{ icon: Eye }} |
| 320 | + on:click={async () => { |
| 321 | + const jobId = pendingsJobs.find((pendingsJob) => pendingsJob.id === selectedJobId)?.id |
| 322 | + if (jobId) { |
| 323 | + const job = await JobService.getJob({ workspace: $workspaceStore!, id: jobId }) |
| 324 | + codeObj = { |
| 325 | + content: job.raw_code ?? '', |
| 326 | + language: job.language ?? 'bash' |
| 327 | + } |
| 328 | + codeViewer?.openDrawer() |
| 329 | + } else { |
| 330 | + pendingsJobs = pendingsJobs.filter((pendingJob) => pendingJob.id !== selectedJobId) |
| 331 | + selectedJobId = '' |
| 332 | + } |
| 333 | + }} |
| 334 | + iconOnly |
| 335 | + /> |
| 336 | + <Button |
| 337 | + loading={isCancelingJob} |
| 338 | + disabled={emptyString(selectedJobId)} |
| 339 | + color="red" |
| 340 | + size="xs" |
| 341 | + variant="border" |
| 342 | + startIcon={{ icon: Square }} |
| 343 | + on:click={async () => { |
| 344 | + try { |
| 345 | + isCancelingJob = true |
| 346 | + await cancelJob(selectedJobId) |
| 347 | + sendUserToast('Job cancelled successfully') |
| 348 | + pendingsJobs = pendingsJobs.filter((pendingJob) => pendingJob.id !== selectedJobId) |
| 349 | + selectedJobId = '' |
| 350 | + } catch (error) { |
| 351 | + sendUserToast(error.body || error.message, true) |
| 352 | + await listPendingJobsAndUpdateSelectedJobid() |
| 353 | + } finally { |
| 354 | + isCancelingJob = false |
| 355 | + } |
| 356 | + }} |
| 357 | + iconOnly |
| 358 | + /> |
| 359 | + </div> |
| 360 | + {/if} |
| 361 | + </div> |
| 362 | + |
| 363 | + <div> |
226 | 364 | <div class="flex justify-start w-full mb-2">
|
227 | 365 | <div class="flex flex-row">
|
228 | 366 | <Badge
|
|
239 | 377 | </div>
|
240 | 378 | <input type="text" disabled bind:value={working_directory} />
|
241 | 379 | </div>
|
242 |
| - </div> |
243 | 380 |
|
244 |
| - <div bind:this={container}></div> |
| 381 | + <div bind:this={container}></div> |
| 382 | + </div> |
245 | 383 | </div>
|
246 | 384 | <div class="flex flex-col h-full gap-1 mt-2">
|
247 | 385 | <div class="flex flex-row w-full justify-between">
|
|
0 commit comments