chore: load paginated versions of workflow

This commit is contained in:
Abhishek Kumar 2026-05-07 13:43:59 +05:30
parent d2a119c38a
commit 0282eb3225
9 changed files with 93 additions and 14 deletions

View file

@ -300,10 +300,18 @@ class WorkflowClient(BaseDBClient):
async def get_workflow_versions(
self,
workflow_id: int,
limit: int | None = None,
offset: int = 0,
) -> list[WorkflowDefinitionModel]:
"""List all versions for a workflow, newest first."""
"""List versions for a workflow, newest first.
When `limit` is provided, returns at most `limit` rows starting from
`offset` used by the version history panel to page through long
histories without dragging the full `workflow_json` payload for every
version on every open.
"""
async with self.async_session() as session:
result = await session.execute(
query = (
select(WorkflowDefinitionModel)
.where(
WorkflowDefinitionModel.workflow_id == workflow_id,
@ -313,6 +321,11 @@ class WorkflowClient(BaseDBClient):
)
.order_by(WorkflowDefinitionModel.version_number.desc())
)
if offset:
query = query.offset(offset)
if limit is not None:
query = query.limit(limit)
result = await session.execute(query)
return result.scalars().all()
async def get_all_workflows(

View file

@ -703,9 +703,15 @@ async def get_workflow(
@router.get("/{workflow_id}/versions")
async def get_workflow_versions(
workflow_id: int,
limit: int | None = Query(None, ge=1, le=100),
offset: int = Query(0, ge=0),
user: UserModel = Depends(get_user),
) -> list[WorkflowVersionResponse]:
"""List all versions for a workflow, newest first."""
"""List versions for a workflow, newest first.
Pass `limit`/`offset` to page through long histories. With no `limit`,
returns every version (legacy behavior).
"""
workflow = await db_client.get_workflow(
workflow_id, organization_id=user.selected_organization_id
)
@ -714,7 +720,9 @@ async def get_workflow_versions(
status_code=404, detail=f"Workflow with id {workflow_id} not found"
)
versions = await db_client.get_workflow_versions(workflow_id)
versions = await db_client.get_workflow_versions(
workflow_id, limit=limit, offset=offset
)
return [
WorkflowVersionResponse(
id=v.id,

View file

@ -47,7 +47,7 @@ class BusyWaitProcessor(FrameProcessor):
@pytest.mark.asyncio
async def test_interruption_with_blocked_end_frame():
busy_wait_processor = BusyWaitProcessor(wait_time=0.5)
busy_wait_processor = BusyWaitProcessor(wait_time=5.0)
transport = MockTransport()
pipeline = Pipeline([transport, busy_wait_processor])
@ -84,8 +84,6 @@ async def test_interruption_with_blocked_end_frame():
)
# If there are pending tasks, we timed out
# FIXME: Currently I have creaetd an issue on pipecat which talks about
# how this behaviour is not good. https://github.com/pipecat-ai/pipecat/issues/4412
if pending:
# Cancel all pending tasks
for t in pending:

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit e8f59a6a58977f4f5598e8de7bfbbf9b6d474eb8
Subproject commit 97b3b041bda0099dbe48b6f20daf49ce113711f3

View file

@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: dograh-openapi-XXXXXX.json.r8rR0xozEB
# timestamp: 2026-05-06T12:17:39+00:00
# filename: dograh-openapi-XXXXXX.json.x7226hNjaY
# timestamp: 2026-05-07T08:13:29+00:00
from __future__ import annotations

View file

@ -39,6 +39,8 @@ const edgeTypes = {
custom: CustomEdge,
};
const VERSIONS_PAGE_SIZE = 10;
interface RenderWorkflowProps {
initialWorkflowName: string;
workflowId: number;
@ -65,6 +67,8 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
const [isVersionPanelOpen, setIsVersionPanelOpen] = useState(false);
const [versions, setVersions] = useState<WorkflowVersion[]>([]);
const [versionsLoading, setVersionsLoading] = useState(false);
const [versionsLoadingMore, setVersionsLoadingMore] = useState(false);
const [versionsHasMore, setVersionsHasMore] = useState(false);
const [activeVersionId, setActiveVersionId] = useState<number | null>(null);
// Version info that updates immediately from the GET/save/publish responses.
const [currentVersionNumber, setCurrentVersionNumber] = useState<number | null>(initialVersionNumber ?? null);
@ -104,18 +108,24 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
// Derive hasDraft from the current version status
const hasDraft = currentVersionStatus === "draft";
// Fetch workflow versions, optionally forcing a refresh
// Fetch the first page of workflow versions, optionally forcing a refresh.
// Pagination keeps the panel snappy when a workflow has accumulated a long
// history — `workflow_json` is shipped per row, so loading hundreds at once
// is expensive on the wire.
const fetchVersions = useCallback(async (force = false) => {
if (versionsFetched.current && !force) return;
setVersionsLoading(true);
try {
const response = await getWorkflowVersionsApiV1WorkflowWorkflowIdVersionsGet({
path: { workflow_id: workflowId },
query: { limit: VERSIONS_PAGE_SIZE, offset: 0 },
});
const data = response.data as WorkflowVersion[] | undefined;
if (data) {
setVersions(data);
// Set active version to draft if exists, else published
setVersionsHasMore(data.length === VERSIONS_PAGE_SIZE);
// Set active version to draft if exists, else published.
// Both live on the newest page so the first fetch always sees them.
const current = data.find((v) => v.status === "draft") ?? data.find((v) => v.status === "published");
if (current) {
setActiveVersionId(current.id);
@ -129,6 +139,24 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
}
}, [workflowId]);
const handleLoadMoreVersions = useCallback(async () => {
if (versionsLoadingMore || !versionsHasMore) return;
setVersionsLoadingMore(true);
try {
const response = await getWorkflowVersionsApiV1WorkflowWorkflowIdVersionsGet({
path: { workflow_id: workflowId },
query: { limit: VERSIONS_PAGE_SIZE, offset: versions.length },
});
const data = response.data as WorkflowVersion[] | undefined;
if (data) {
setVersions((prev) => [...prev, ...data]);
setVersionsHasMore(data.length === VERSIONS_PAGE_SIZE);
}
} finally {
setVersionsLoadingMore(false);
}
}, [workflowId, versions.length, versionsLoadingMore, versionsHasMore]);
const handleOpenVersionPanel = useCallback(() => {
setIsVersionPanelOpen(true);
fetchVersions();
@ -484,6 +512,9 @@ function RenderWorkflow({ initialWorkflowName, workflowId, workflowUuid, initial
loading={versionsLoading}
activeVersionId={activeVersionId}
onSelectVersion={handleSelectVersion}
hasMore={versionsHasMore}
loadingMore={versionsLoadingMore}
onLoadMore={handleLoadMoreVersions}
/>
<PhoneCallDialog

View file

@ -24,6 +24,9 @@ interface VersionHistoryPanelProps {
loading: boolean;
activeVersionId: number | null;
onSelectVersion: (version: WorkflowVersion) => void;
hasMore: boolean;
loadingMore: boolean;
onLoadMore: () => void;
}
const statusLabel: Record<string, string> = {
@ -45,6 +48,9 @@ export const VersionHistoryPanel = ({
loading,
activeVersionId,
onSelectVersion,
hasMore,
loadingMore,
onLoadMore,
}: VersionHistoryPanelProps) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@ -125,6 +131,20 @@ export const VersionHistoryPanel = ({
</button>
);
})}
{hasMore && (
<Button
variant="ghost"
onClick={onLoadMore}
disabled={loadingMore}
className="w-full text-sm text-gray-300 hover:text-white hover:bg-[#2a2a2a]"
>
{loadingMore ? (
<LoaderCircle className="w-4 h-4 animate-spin" />
) : (
"Load more"
)}
</Button>
)}
</div>
)}
</div>

View file

@ -5694,7 +5694,16 @@ export type GetWorkflowVersionsApiV1WorkflowWorkflowIdVersionsGetData = {
*/
workflow_id: number;
};
query?: never;
query?: {
/**
* Limit
*/
limit?: number | null;
/**
* Offset
*/
offset?: number;
};
url: '/api/v1/workflow/{workflow_id}/versions';
};