-
Notifications
You must be signed in to change notification settings - Fork 170
feat(drive-integration): async import runs rearchitecture [INTEG-4526] #11092
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 6 commits
e60283d
8fac7a8
2d23ce1
cd4c650
66a8b7d
db5e939
82d7a99
de91278
0c40a28
51e55db
d728407
9b2fe4c
9719b01
32e8e76
ab88836
c6fdfc7
06780ed
3bf3a10
d46abe4
6b49900
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import { useState, useCallback } from 'react'; | ||
| import type { RunRecord } from '../types/runs'; | ||
|
|
||
| const MAX_RUNS = 50; | ||
|
|
||
| function makeKey(spaceId: string, environmentId: string): string { | ||
| return `gdrive-import-runs::${spaceId}::${environmentId}`; | ||
| } | ||
|
|
||
| function readFromStorage(key: string): RunRecord[] { | ||
| try { | ||
| const raw = localStorage.getItem(key); | ||
| if (!raw) return []; | ||
| return JSON.parse(raw) as RunRecord[]; | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| function writeToStorage(key: string, records: RunRecord[]): void { | ||
| localStorage.setItem(key, JSON.stringify(records)); | ||
| } | ||
|
|
||
| export interface UseRunStorage { | ||
| runs: RunRecord[]; | ||
| addRun(record: RunRecord): void; | ||
| removeRun(runId: string): void; | ||
| markCompleted(runId: string, entryIds: string[]): void; | ||
| storageError: string | null; | ||
| } | ||
|
|
||
| export function useRunStorage(spaceId: string, environmentId: string): UseRunStorage { | ||
| const key = makeKey(spaceId, environmentId); | ||
| const [runs, setRuns] = useState<RunRecord[]>(() => readFromStorage(key)); | ||
| const [storageError, setStorageError] = useState<string | null>(null); | ||
|
|
||
| const persist = useCallback( | ||
| (next: RunRecord[]) => { | ||
| try { | ||
| writeToStorage(key, next); | ||
| setStorageError(null); | ||
| } catch (err) { | ||
| const msg = | ||
| err instanceof Error | ||
| ? err.message | ||
| : 'Unable to save import history. Storage may be full.'; | ||
| setStorageError(msg); | ||
| } | ||
| setRuns(next); | ||
| }, | ||
| [key] | ||
| ); | ||
|
|
||
| const addRun = useCallback( | ||
| (record: RunRecord) => { | ||
| setRuns((current) => { | ||
| if (current.some((r) => r.runId === record.runId)) return current; | ||
| const next = [record, ...current]; | ||
| if (next.length > MAX_RUNS) next.splice(MAX_RUNS); | ||
| try { | ||
| writeToStorage(key, next); | ||
| setStorageError(null); | ||
| } catch (err) { | ||
| const msg = | ||
| err instanceof Error | ||
| ? err.message | ||
| : 'Unable to save import history. Storage may be full.'; | ||
| setStorageError(msg); | ||
| } | ||
| return next; | ||
| }); | ||
| }, | ||
| [key] | ||
| ); | ||
|
|
||
| const removeRun = useCallback( | ||
| (runId: string) => { | ||
| setRuns((current) => { | ||
| const next = current.filter((r) => r.runId !== runId); | ||
| persist(next); | ||
| return next; | ||
| }); | ||
| }, | ||
| [persist] | ||
| ); | ||
|
|
||
| const markCompleted = useCallback( | ||
| (runId: string, entryIds: string[]) => { | ||
| setRuns((current) => { | ||
| const next = current.map((r) => | ||
| r.runId === runId ? { ...r, createdEntryIds: entryIds } : r | ||
| ); | ||
| persist(next); | ||
| return next; | ||
| }); | ||
| }, | ||
| [persist] | ||
| ); | ||
|
|
||
| return { runs, addRun, removeRun, markCompleted, storageError }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import { useState, useEffect, useRef, useCallback } from 'react'; | ||
| import { PageAppSDK } from '@contentful/app-sdk'; | ||
| import { RunStatus } from '@types'; | ||
| import { getWorkflowRun, AgentRunData } from '../services/agents-api'; | ||
| import type { DisplayStatus, RunRecord } from '../types/runs'; | ||
|
|
||
| const POLL_INTERVAL_MS = 10_000; | ||
|
|
||
| function toDisplayStatus(runData: AgentRunData | null): DisplayStatus { | ||
| if (!runData) return 'expired'; | ||
| const status = runData.sys?.status ?? runData.metadata?.status; | ||
| switch (status) { | ||
| case RunStatus.IN_PROGRESS: | ||
| case RunStatus.DRAFT: | ||
| return 'running'; | ||
| case RunStatus.PENDING_REVIEW: | ||
| return 'needs-review'; | ||
| case RunStatus.COMPLETED: | ||
| return 'completed'; | ||
| case RunStatus.FAILED: | ||
| return 'failed'; | ||
| default: | ||
| return 'expired'; | ||
| } | ||
| } | ||
|
JuliRossi marked this conversation as resolved.
|
||
|
|
||
| function extractErrorMessage(runData: AgentRunData | null): string | undefined { | ||
| return runData?.metadata?.workflowFailure?.message ?? undefined; | ||
|
JuliRossi marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| export interface UseRunsPollingResult { | ||
| statusMap: Map<string, DisplayStatus>; | ||
| errorMap: Map<string, string>; | ||
| } | ||
|
|
||
| export function useRunsPolling(runs: RunRecord[], sdk: PageAppSDK): UseRunsPollingResult { | ||
| const [statusMap, setStatusMap] = useState<Map<string, DisplayStatus>>( | ||
| () => new Map(runs.map((r) => [r.runId, 'loading' as DisplayStatus])) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 I think we can avoid seeding
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed —
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is still doing the same in the current version |
||
| ); | ||
| const [errorMap, setErrorMap] = useState<Map<string, string>>(new Map()); | ||
| const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null); | ||
|
|
||
| const fetchAllStatuses = useCallback(async () => { | ||
| if (runs.length === 0) return; | ||
|
|
||
| const spaceId = sdk.ids.space; | ||
| const environmentId = sdk.ids.environmentAlias ?? sdk.ids.environment; | ||
|
|
||
| const results = await Promise.all( | ||
| runs.map((r) => getWorkflowRun(sdk, spaceId, environmentId, r.runId)) | ||
| ); | ||
|
|
||
| const nextStatus = new Map<string, DisplayStatus>(); | ||
| const nextErrors = new Map<string, string>(); | ||
|
|
||
| for (let i = 0; i < runs.length; i++) { | ||
| const runId = runs[i].runId; | ||
| const data = results[i]; | ||
| nextStatus.set(runId, toDisplayStatus(data)); | ||
| const errMsg = extractErrorMessage(data); | ||
| if (errMsg) nextErrors.set(runId, errMsg); | ||
| } | ||
|
|
||
| setStatusMap(nextStatus); | ||
| setErrorMap(nextErrors); | ||
| return nextStatus; | ||
| }, [runs, sdk]); | ||
|
|
||
| // Initial fetch + reactive refetch when run list changes | ||
| useEffect(() => { | ||
| // Reset new runs to 'loading' | ||
| setStatusMap((prev) => { | ||
| const next = new Map(prev); | ||
| for (const r of runs) { | ||
| if (!next.has(r.runId)) next.set(r.runId, 'loading'); | ||
| } | ||
| return next; | ||
| }); | ||
|
JuliRossi marked this conversation as resolved.
Outdated
|
||
|
|
||
| void fetchAllStatuses().then((nextStatus) => { | ||
| if (!nextStatus) return; | ||
| const hasRunning = [...nextStatus.values()].some((s) => s === 'running'); | ||
|
|
||
| if (intervalRef.current) clearInterval(intervalRef.current); | ||
|
|
||
| if (hasRunning) { | ||
| intervalRef.current = setInterval(() => { | ||
| void fetchAllStatuses().then((updated) => { | ||
| if (!updated) return; | ||
| const stillRunning = [...updated.values()].some((s) => s === 'running'); | ||
| if (!stillRunning && intervalRef.current) { | ||
| clearInterval(intervalRef.current); | ||
| intervalRef.current = null; | ||
| } | ||
| }); | ||
| }, POLL_INTERVAL_MS); | ||
| } | ||
| }); | ||
|
|
||
| return () => { | ||
| if (intervalRef.current) { | ||
| clearInterval(intervalRef.current); | ||
| intervalRef.current = null; | ||
| } | ||
| }; | ||
| }, [fetchAllStatuses]); | ||
|
|
||
| return { statusMap, errorMap }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.