|
1 | | -import React, { useState, useCallback } from 'react'; |
| 1 | +import React, { useState, useCallback, useMemo, useEffect } from 'react'; |
2 | 2 | import Sheet from '@mui/joy/Sheet'; |
3 | 3 |
|
4 | 4 | import { Button, LinearProgress, Stack, Typography } from '@mui/joy'; |
@@ -40,6 +40,44 @@ export default function Tasks() { |
40 | 40 | const { experimentInfo } = useExperimentInfo(); |
41 | 41 | const { addNotification } = useNotification(); |
42 | 42 |
|
| 43 | + // Pending job IDs persisted per experiment to show immediate placeholders |
| 44 | + const pendingJobsStorageKey = useMemo( |
| 45 | + () => |
| 46 | + experimentInfo?.id |
| 47 | + ? `pendingJobIds:${String(experimentInfo.id)}` |
| 48 | + : 'pendingJobIds:unknown', |
| 49 | + [experimentInfo?.id], |
| 50 | + ); |
| 51 | + useEffect(() => { |
| 52 | + // Debug storage key per experiment |
| 53 | + // eslint-disable-next-line no-console |
| 54 | + }, [pendingJobsStorageKey]); |
| 55 | + |
| 56 | + const getPendingJobIds = useCallback((): string[] => { |
| 57 | + try { |
| 58 | + const raw = window.localStorage.getItem(pendingJobsStorageKey); |
| 59 | + if (!raw) return []; |
| 60 | + const parsed = JSON.parse(raw); |
| 61 | + const result = Array.isArray(parsed) ? parsed : []; |
| 62 | + // eslint-disable-next-line no-console |
| 63 | + return result; |
| 64 | + } catch { |
| 65 | + return []; |
| 66 | + } |
| 67 | + }, [pendingJobsStorageKey]); |
| 68 | + |
| 69 | + const setPendingJobIds = useCallback( |
| 70 | + (ids: string[]) => { |
| 71 | + try { |
| 72 | + window.localStorage.setItem(pendingJobsStorageKey, JSON.stringify(ids)); |
| 73 | + // eslint-disable-next-line no-console |
| 74 | + } catch { |
| 75 | + // ignore storage failures |
| 76 | + } |
| 77 | + }, |
| 78 | + [pendingJobsStorageKey], |
| 79 | + ); |
| 80 | + |
43 | 81 | const handleOpen = () => setModalOpen(true); |
44 | 82 | const handleClose = () => setModalOpen(false); |
45 | 83 | const handleEditClose = () => { |
@@ -104,6 +142,45 @@ export default function Tasks() { |
104 | 142 |
|
105 | 143 | const loading = tasksIsLoading || jobsIsLoading; |
106 | 144 |
|
| 145 | + // Remove any pending placeholders that are now present in jobs |
| 146 | + useEffect(() => { |
| 147 | + if (!jobs || !Array.isArray(jobs)) return; |
| 148 | + const pending = getPendingJobIds(); |
| 149 | + if (pending.length === 0) return; |
| 150 | + const existingIds = new Set((jobs as any[]).map((j: any) => String(j.id))); |
| 151 | + const stillPending = pending.filter((id) => !existingIds.has(String(id))); |
| 152 | + // eslint-disable-next-line no-console |
| 153 | + console.log('[Tasks] prune pending vs jobs', { |
| 154 | + jobsCount: jobs?.length, |
| 155 | + pending, |
| 156 | + stillPending, |
| 157 | + }); |
| 158 | + if (stillPending.length !== pending.length) { |
| 159 | + setPendingJobIds(stillPending); |
| 160 | + } |
| 161 | + }, [jobs, getPendingJobIds, setPendingJobIds]); |
| 162 | + |
| 163 | + // Build list with placeholders for pending job IDs not yet in jobs |
| 164 | + const jobsWithPlaceholders = useMemo(() => { |
| 165 | + const baseJobs = Array.isArray(jobs) ? jobs : []; |
| 166 | + const pending = getPendingJobIds(); |
| 167 | + if (!pending.length) return baseJobs; |
| 168 | + const existingIds = new Set(baseJobs.map((j: any) => String(j.id))); |
| 169 | + const placeholders = pending |
| 170 | + .filter((id) => !existingIds.has(String(id))) |
| 171 | + .map((id) => ({ |
| 172 | + id: String(id), |
| 173 | + type: 'REMOTE', |
| 174 | + status: 'CREATED', |
| 175 | + progress: 0, |
| 176 | + job_data: {}, |
| 177 | + placeholder: true, |
| 178 | + })); |
| 179 | + // Show newest first consistent with existing ordering if any |
| 180 | + const combined = [...placeholders, ...baseJobs]; |
| 181 | + return combined; |
| 182 | + }, [jobs, getPendingJobIds]); |
| 183 | + |
107 | 184 | const handleDeleteTask = async (taskId: string) => { |
108 | 185 | if (!experimentInfo?.id) return; |
109 | 186 |
|
@@ -296,9 +373,14 @@ export default function Tasks() { |
296 | 373 | const createJobResult = await createJobResp.json(); |
297 | 374 |
|
298 | 375 | if (createJobResult.status === 'success') { |
299 | | - // Keep placeholder visible and refresh jobs list |
300 | | - // The placeholder will be replaced when the real job appears |
301 | | - await jobsMutate(); |
| 376 | + // Persist pending placeholder immediately so it shows up in the UI |
| 377 | + const newId = String(createJobResult.job_id); |
| 378 | + const pending = getPendingJobIds(); |
| 379 | + if (!pending.includes(newId)) { |
| 380 | + setPendingJobIds([newId, ...pending]); |
| 381 | + } |
| 382 | + // IMPORTANT: Don't await jobsMutate, let UI update before real jobs arrive |
| 383 | + setTimeout(() => jobsMutate(), 0); |
302 | 384 |
|
303 | 385 | addNotification({ |
304 | 386 | type: 'success', |
@@ -412,7 +494,7 @@ export default function Tasks() { |
412 | 494 | <LinearProgress /> |
413 | 495 | ) : ( |
414 | 496 | <JobsList |
415 | | - jobs={jobs} |
| 497 | + jobs={jobsWithPlaceholders as any} |
416 | 498 | onDeleteJob={handleDeleteJob} |
417 | 499 | onViewOutput={(jobId) => setViewOutputFromJob(parseInt(jobId))} |
418 | 500 | onViewTensorboard={(jobId) => |
|
0 commit comments