Skip to content

UN-3770 [FIX] Make list pagination consistent across shared resource endpoints#2208

Open
chandrasekharan-zipstack wants to merge 2 commits into
mainfrom
feat/list-pagination-consistency
Open

UN-3770 [FIX] Make list pagination consistent across shared resource endpoints#2208
chandrasekharan-zipstack wants to merge 2 commits into
mainfrom
feat/list-pagination-consistency

Conversation

@chandrasekharan-zipstack

Copy link
Copy Markdown
Contributor

What

#2187 added opt-in pagination to workflows, prompt studio, adapters and connectors, but only the Workflows page got wired up. This finishes the job for the other three and removes the thing that blocked them.

Why the rollout stalled

The adapter, connector and prompt studio for_user() managers used DISTINCT ON. Postgres requires a DISTINCT ON query to order by the distinct expression first, so those three viewsets were pinned to order_by("id") and physically could not order by modified_at. Workflows shipped only because its manager already used plain .distinct().

Git archaeology says this is copy-paste lineage, not design — 8a081fa2 (#465) introduced it for adapters and it was copied into #471 and #1476, while #1462 independently used plain .distinct() for workflows.

Backend

  • Managers.distinct("id") / .distinct("tool_id") → plain .distinct(). Every arm of the sharing predicate is a PK subquery (resources_visible_via_groups / resources_visible_via_memberships), not a join, so there are no duplicate rows to collapse. The swap is behaviour-preserving; Workflows is the live proof.
  • Ordering — replaced per-view order_by() with declarative ordering = ["-modified_at", "pk"]. OrderingFilter is already in DEFAULT_FILTER_BACKENDS, so no filter_backends override is needed — important, because overriding it would silently drop OrganizationFilterBackend.
  • Dropped ?order_by=asc|desc from the Workflow viewset. It was added in UN-3770 [FEAT] Opt-in pagination for list endpoints; wire Workflows page #2187 and has zero consumers in either repo; ?ordering= covers it through the standard filter.

Frontend

  • New helpers/pagination.js: unwrapList (bare array or envelope) and fetchAllPages (pages to exhaustion, single request when unpaginated).
  • All adapter/connector/workflow selectors route through fetchAllPages — otherwise they'd silently show only the first 50 rows after the flip.
  • Prompt Studio, adapters and connectors listing pages converted to server-side pagination + search via usePaginatedList. The client-side useListSearch filter is deleted.
  • <Pagination> moved into ViewTools, so all four pages share one implementation — page size 10, size changer on, ["10","20","50"]. Workflows had showSizeChanger={false}; that inconsistency is gone.
  • Fixed Workflows.jsx assigning fetchListRef.current during render.

Safety

Endpoints remain opt-in paginated in this PR, so every change works against both response shapes. Flipping to unconditional CustomPagination is deliberately a separate ~3-line PR, after this is validated on staging.

Ordered separately: unstract-python-client#24 (clone script page-following) and the matching cloud-plugin PR.

Tests

backend/utils/tests/test_list_pagination.py — 3 tests parametrized across all four endpoints (12 subtests):

  1. pages partition the result set (no repeats, nothing dropped)
  2. a row reachable via several sharing predicates lists once and count matches
  3. ?search= narrows both rows and count

Verified these actually fail without the fix: restoring .distinct("id") makes Postgres reject the query outright.

  • backend sweep (utils/, permissions/, four resource apps): 293 passed, 28 skipped
  • frontend: 22 passed, including 6 new for the pagination helpers
  • frontend build verified with cloud plugins copied in, so the cross-repo imports resolve

🤖 Generated with Claude Code

https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ

…endpoints

#2187 added opt-in pagination to workflows, prompt studio, adapters and
connectors but only wired the Workflows page. The other three could not
follow: their `for_user()` managers used `DISTINCT ON`, which forces
Postgres to order by the distinct expression, so those viewsets were
pinned to `order_by("id")` and could not order by `modified_at`.

Backend
- Swap `.distinct("id")` / `.distinct("tool_id")` for plain `.distinct()`
  in the adapter, connector and prompt studio managers. Every arm of the
  sharing predicate is a PK subquery, not a join, so no duplicate rows
  exist to collapse and the swap is behaviour-preserving. Workflows has
  shipped this way since #1462.
- Replace the per-view `order_by()` calls with a declarative
  `ordering = ["-modified_at", "pk"]`. `OrderingFilter` is already in
  `DEFAULT_FILTER_BACKENDS`, so this needs no `filter_backends` override
  (which would drop `OrganizationFilterBackend`).
- Drop Workflow's `?order_by=asc|desc`; it has no consumer, and
  `?ordering=` now covers it through the standard filter.

Frontend
- Add `unwrapList` / `fetchAllPages` helpers. Selectors page to
  exhaustion rather than silently showing only the first 50 rows.
- Route all adapter, connector and workflow selectors through them.
- Convert the Prompt Studio, adapters and connectors pages to
  server-side pagination and search via `usePaginatedList`, replacing the
  client-side `useListSearch` filter (now deleted).
- Move `<Pagination>` into `ViewTools` so all four pages share one
  implementation: page size 10, size changer on, `["10","20","50"]`.
  Workflows had the changer disabled; that inconsistency goes away.
- Stop assigning `fetchListRef.current` during render in Workflows.

Endpoints stay opt-in paginated here, so every change is safe against
both response shapes. Flipping them to unconditional `CustomPagination`
is a separate change, after this has been validated on staging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added unified server-side pagination and search for workflows, connectors, adapters, and custom tools, including page-size controls and totals.
    • Implemented “fetch all pages” loading for dropdowns/selectors and added shared pagination helpers and a usePaginatedResource hook.
    • Added deterministic sorting with stable pk tie-breaking across shared lists.
  • Bug Fixes
    • Improved deduplication semantics and ensured deterministic pagination/sorting behavior.
    • Fixed count/search interactions so count reflects the searched subset.
  • Tests
    • Added backend list-pagination contract tests and frontend unit tests for pagination helpers and the new hook.

Walkthrough

Backend list APIs now use deterministic ordering and full-row deduplication. Frontend list consumers share pagination helpers and paginated list state for multi-page fetching, search, refreshes, and pagination controls.

Changes

List pagination consistency

Layer / File(s) Summary
Backend ordering and deduplication
backend/*/models.py, backend/*/views.py, backend/backend/settings/base.py, backend/utils/filters/*
Shared list managers use distinct(), and viewsets use deterministic -modified_at, pk ordering with explicit sortable fields through the custom ordering filter.
Backend pagination contract validation
backend/utils/tests/test_list_pagination.py
Tests verify page partitioning, tie-break ordering, duplicate-share elimination, and search-aware counts across workflows, tools, adapters, and connectors.
Shared frontend pagination primitives
frontend/src/helpers/pagination.*, frontend/src/hooks/usePaginatedResource.*
List responses are normalized and aggregated across pages, while the shared hook manages server-side pagination, search, refreshes, empty-page fallback, errors, and stale requests.
Frontend list and fetch integration
frontend/src/components/workflows/..., frontend/src/components/custom-tools/..., frontend/src/components/tool-settings/..., frontend/src/pages/ConnectorsPage.jsx, frontend/src/components/agency/..., frontend/src/components/settings/...
List screens use paginated state and shared controls, mutations refresh the active view, and adapter, connector, and workflow retrieval paths fetch all pages.
Shared list rendering
frontend/src/components/custom-tools/view-tools/*
ViewTools accepts optional pagination data and renders the configured pagination control and styling.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ListComponent
  participant usePaginatedResource
  participant API
  participant Database
  User->>ListComponent: search or change page
  ListComponent->>usePaginatedResource: update pagination or search
  usePaginatedResource->>API: request filtered page
  API->>Database: apply filters, distinct, and deterministic ordering
  Database-->>API: return results and count
  API-->>usePaginatedResource: return paginated response
  usePaginatedResource-->>ListComponent: update rows and pagination
  ListComponent-->>User: render list and controls
Loading

Suggested reviewers: kirtimanmishrazipstack

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change well, but it misses required template sections like How, breakage, migrations, env config, and the checklist. Add the missing template sections, especially How, breakage impact, database migrations, env config, relevant docs, testing notes, screenshots, and checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 24.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change to shared resource pagination consistency.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/list-pagination-consistency

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chandrasekharan-zipstack
chandrasekharan-zipstack marked this pull request as ready for review July 24, 2026 11:56
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR standardizes deterministic, server-side pagination across shared resource endpoints.

  • Replaces PostgreSQL-specific DISTINCT ON queries with plain distinct() for adapters, connectors, and Prompt Studio resources.
  • Adds deterministic default and request-selected ordering with a primary-key tie-breaker.
  • Introduces shared frontend pagination helpers and a race-safe paginated-resource hook.
  • Migrates workflow, Prompt Studio, adapter, and connector listings and selectors to support paginated response envelopes.
  • Adds backend pagination contract tests and frontend helper/hook tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failures remain in the fixes related to the previous review threads.

Important Files Changed

Filename Overview
frontend/src/hooks/usePaginatedResource.js Centralizes paginated list state, empty-page fallback, search, refresh behavior, and stale-response suppression.
backend/utils/filters/ordering_filter.py Extends DRF ordering so request-selected non-unique fields retain a primary-key tie-breaker.
frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx Migrates adapter settings to the shared server-side pagination hook and incorporates the prior stale-response fix.
backend/adapter_processor_v2/views.py Enables deterministic modified-time ordering and supported client ordering for paginated adapter lists.
backend/utils/tests/test_list_pagination.py Adds shared endpoint coverage for page partitioning, deterministic custom ordering, deduplication, and search counts.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    UI[Shared resource list UI] --> Hook[usePaginatedResource]
    Hook -->|page, page_size, search| API[Django list endpoint]
    API --> Filter[Search and deterministic ordering]
    Filter --> Page[OptionalPagination]
    Page -->|count and results| Hook
    Hook --> View[ViewTools and Pagination]
    Selector[Selector or dropdown] --> AllPages[fetchAllPages]
    AllPages --> API
    AllPages -->|flattened resources| Selector
Loading

Reviews (2): Last reviewed commit: "UN-3770 [FIX] Address review: pk tie-bre..." | Re-trigger Greptile

Comment thread frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx Outdated
Comment thread backend/adapter_processor_v2/views.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx (1)

145-177: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Same unreturned-promise "step back a page" issue as Workflows.jsx/ListOfTools.jsx.

getAdapters doesn't return its axiosPrivate({...}).then()... chain, so the recursive call at line 165 also isn't returned; the outer .finally clears isLoading before the recursive fetch resolves. See consolidated comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx` around
lines 145 - 177, Update getAdapters to return the axiosPrivate promise chain,
including the recursive getAdapters call when stepping back from an empty page,
so the outer finally runs only after the replacement fetch resolves.
frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx (1)

157-195: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Same unreturned-promise "step back a page" issue as Workflows.jsx.

getListOfTools doesn't return the axiosPrivate({...}).then()... chain, so the recursive call at line 181 also isn't returned; setIsListLoading(false) in the outer .finally fires before the recursive fetch resolves. See consolidated comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx` around
lines 157 - 195, Update getListOfTools to return the axiosPrivate promise chain,
and return the recursive getListOfTools call when stepping back from an empty
page. This ensures the outer finally waits for the replacement-page request
before clearing isListLoading.
frontend/src/components/workflows/workflow/Workflows.jsx (1)

115-151: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Loading flag clears before the "step back a page" recursion finishes.

getProjectList never returns its projectApiService.getProjectList(params).then()... chain, and the recursive call at line 132 isn't returned either. The outer .finally(() => setLoading(false)) therefore resolves immediately after triggering the recursive fetch, not after it completes — loading briefly goes false while stale/empty data is still on screen. See consolidated comment for the shared fix across files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/workflows/workflow/Workflows.jsx` around lines 115 -
151, Return the projectApiService.getProjectList(params) promise chain from
getProjectList, and return the recursive getProjectList(page - 1, pageSize,
search) call in the empty-page branch so the outer finally runs only after the
replacement fetch completes. Preserve the existing pagination and loading
behavior otherwise.
🧹 Nitpick comments (1)
frontend/src/components/workflows/workflow/workflow-service.js (1)

14-19: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Pass an explicit page_size on the full list fetch.

WorkflowViewSet.list maps to /workflow/ with OptionalPagination, whose default is 50, and fetchAllPages keeps requesting page until the result set is complete. Since this selector is mounted from EtlTaskDeploy on app deployment, a larger page_size such as 100 is appropriate for this dropdown.

💡 Suggested tweak
     getWorkflowList: () =>
       fetchAllPages(axiosPrivate, {
         url: `${path}/workflow/`,
-        params: { is_active: "True" },
+        params: { is_active: "True", page_size: 100 },
       }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/workflows/workflow/workflow-service.js` around lines
14 - 19, Update getWorkflowList to include an explicit page_size of 100 in the
params passed to fetchAllPages, while preserving the existing is_active filter
and pagination behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/adapter_processor_v2/views.py`:
- Around line 151-153: Ensure client-supplied ordering retains a deterministic
pk tie-breaker before pagination: update the ordering configuration/filter
behavior at backend/adapter_processor_v2/views.py lines 151-153,
backend/connector_v2/views.py lines 50-52,
backend/prompt_studio/prompt_studio_core_v2/views.py lines 136-138, and
backend/workflow_manager/workflow_v2/views.py lines 80-82 so selected ordering
always appends pk while preserving direction and avoiding duplicate pk entries.

In `@backend/utils/tests/test_list_pagination.py`:
- Around line 126-149: Update test_pages_partition_the_result_set to assign
controlled modified_at values and assert each page’s result sequence follows the
declared -modified_at, pk ordering, not only set membership. Add coverage for
ordering=modified_at using tied timestamps, verifying the returned sequence and
deterministic pk tie-breaker across pages.

In `@frontend/src/pages/ConnectorsPage.jsx`:
- Around line 113-138: Update the recursive fallback in fetchConnectors so it
awaits the fetchConnectors(page - 1, pageSize, search) call before returning,
ensuring the surrounding finally block keeps loading active until the recursive
request completes.

---

Outside diff comments:
In `@frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx`:
- Around line 157-195: Update getListOfTools to return the axiosPrivate promise
chain, and return the recursive getListOfTools call when stepping back from an
empty page. This ensures the outer finally waits for the replacement-page
request before clearing isListLoading.

In `@frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx`:
- Around line 145-177: Update getAdapters to return the axiosPrivate promise
chain, including the recursive getAdapters call when stepping back from an empty
page, so the outer finally runs only after the replacement fetch resolves.

In `@frontend/src/components/workflows/workflow/Workflows.jsx`:
- Around line 115-151: Return the projectApiService.getProjectList(params)
promise chain from getProjectList, and return the recursive getProjectList(page
- 1, pageSize, search) call in the empty-page branch so the outer finally runs
only after the replacement fetch completes. Preserve the existing pagination and
loading behavior otherwise.

---

Nitpick comments:
In `@frontend/src/components/workflows/workflow/workflow-service.js`:
- Around line 14-19: Update getWorkflowList to include an explicit page_size of
100 in the params passed to fetchAllPages, while preserving the existing
is_active filter and pagination behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 49d33f54-a099-4e50-8afb-254c46c4760d

📥 Commits

Reviewing files that changed from the base of the PR and between 023b140 and 45bf279.

📒 Files selected for processing (26)
  • backend/adapter_processor_v2/models.py
  • backend/adapter_processor_v2/views.py
  • backend/connector_v2/models.py
  • backend/connector_v2/views.py
  • backend/prompt_studio/prompt_studio_core_v2/models.py
  • backend/prompt_studio/prompt_studio_core_v2/views.py
  • backend/utils/tests/test_list_pagination.py
  • backend/workflow_manager/workflow_v2/views.py
  • frontend/src/components/agency/configure-connector-modal/ConfigureConnectorModal.jsx
  • frontend/src/components/custom-tools/adapter-selection-modal/AdapterSelectionModal.jsx
  • frontend/src/components/custom-tools/add-llm-profile/AddLlmProfile.jsx
  • frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx
  • frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx
  • frontend/src/components/custom-tools/view-tools/ViewTools.css
  • frontend/src/components/custom-tools/view-tools/ViewTools.jsx
  • frontend/src/components/helpers/custom-tools/CustomToolsHelper.js
  • frontend/src/components/pipelines-or-deployments/etl-task-deploy/EtlTaskDeploy.jsx
  • frontend/src/components/settings/default-triad/DefaultTriad.jsx
  • frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx
  • frontend/src/components/workflows/workflow/Workflows.css
  • frontend/src/components/workflows/workflow/Workflows.jsx
  • frontend/src/components/workflows/workflow/workflow-service.js
  • frontend/src/helpers/pagination.js
  • frontend/src/helpers/pagination.test.js
  • frontend/src/hooks/useListSearch.js
  • frontend/src/pages/ConnectorsPage.jsx
💤 Files with no reviewable changes (2)
  • frontend/src/hooks/useListSearch.js
  • frontend/src/components/workflows/workflow/Workflows.css

Comment thread backend/adapter_processor_v2/views.py
Comment thread backend/utils/tests/test_list_pagination.py Outdated
Comment thread frontend/src/pages/ConnectorsPage.jsx Outdated
…ed list hook

Backend:
- DeterministicOrderingFilter appends `pk` to whatever ordering is in
  effect. `?ordering=` replaces the view's `ordering` outright, so the
  tie-breaker has to be applied by the filter rather than declared on the
  view. Swapped into DEFAULT_FILTER_BACKENDS; a no-op for views that
  declare no ordering and receive no `?ordering=`.
- Tests assert the returned sequence rather than set membership, and pin
  the tie-breaker with rows sharing one `modified_at`. Both fail without
  the filter.

Frontend:
- usePaginatedResource owns the request for all four list pages: params,
  unwrapping, the empty-page step back, and the loading flag. Replaces
  four copy-pasted fetch functions (the Sonar duplication) and the
  fetchListRef indirection.
- Only the newest request may write state, so a slow response can no
  longer restore the previous page, search term or adapter type.
- The loading flag is held by the superseding request, so it no longer
  clears while the step-back page is still in flight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HXYFVCZGi7YN9qjDGyJCQ
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

All checks passed! No linting or formatting issues found.

@chandrasekharan-zipstack

Copy link
Copy Markdown
Contributor Author

Review round addressed in c2ec0dcf

Accepted

Finding Fix
?ordering= drops the pk tie-breaker (Greptile P2 / CodeRabbit major, 4 files) DeterministicOrderingFilter appends pk in get_ordering, installed via DEFAULT_FILTER_BACKENDS
Test asserts page membership, not ordering Sequence assertions + a tied-timestamp ?ordering= test
Stale responses overwrite active lists (Greptile P1) Request-id guard in the new shared hook
Loading flag clears before the step-back page lands (CodeRabbit, 4 files) Same hook — the superseding request owns the flag

Rejected

Nitpick: pass page_size: 100 on getWorkflowList. This would make things worse. OptionalPagination engages only when page or page_size is present, so the current param-less call returns every workflow as a bare array in one request. Adding page_size switches pagination on and turns that single request into a 100-row paginated loop. Left as-is; fetchAllPages handles either shape and will start paging on its own once the CustomPagination flip lands.

Sonar — 7.4% duplication on new code

Valid, and the same root cause as the repeated frontend findings above: 22 duplicated lines each in ConnectorsPage.jsx and ListOfTools.jsx, from a fetch function that was copy-pasted across all four list pages.

Extracted into usePaginatedResource, which owns the request, the page/page_size/search params, response unwrapping, the empty-page step back, in-flight ordering and the loading flag. The four pages lose their fetchListRef indirection and drop 145 lines net. usePaginatedList stays for Pipelines.jsx / ApiDeployment.jsx, which are untouched by this PR.

Verification — backend suite 421 passed / 29 skipped / 36 subtests; frontend 28 tests, build clean. Both new backend tests confirmed to fail with the filter reverted, and the stale-response guard confirmed by mutation.

One note on scope: pipeline_v2/views.py:54 sets filter_backends = [OrderingFilter], which drops OrganizationFilterBackend and its org scoping. Pre-existing and unrelated to pagination, so not touched here — flagging it as worth a separate look.

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 20.7
e2e-coowners e2e 1 0 0 0 1.3
e2e-etl e2e 1 0 0 0 8.4
e2e-login e2e 2 0 0 0 1.5
e2e-prompt-studio e2e 1 0 0 0 4.6
e2e-smoke e2e 2 0 0 0 1.1
e2e-workflow e2e 1 0 0 0 16.8
integration-backend integration 182 0 0 26 42.2
integration-connectors integration 1 0 0 7 8.0
integration-workers integration 0 0 0 141 99.1
unit-backend unit 277 0 0 1 36.6
unit-connectors unit 63 0 0 0 9.8
unit-core unit 33 0 0 0 1.4
unit-platform-service unit 15 0 0 0 2.6
unit-rig unit 86 0 0 0 4.7
unit-sdk1 unit 480 0 0 0 26.3
unit-workers unit 1312 0 0 0 97.1
TOTAL 2460 0 0 175 382.2

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx (1)

171-184: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Creating a new item refreshes the current page instead of jumping to page 1, hiding it under newest-first ordering. Per the PR's deterministic -modified_at ordering, a newly created row always sorts first; refreshing whatever page the user happens to be on (instead of page 1) means the new item is invisible unless the user is already on page 1.

  • frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx#L171-L184: in handleAddNewTool's success handler, only call handleListRefresh() when isEdit; otherwise call fetchPage(1, pagination.pageSize, searchTerm).
  • frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx#L140-L140: addNewItem should branch on whether editItemId was set at call time, refreshing on edit and jumping to page 1 on create.
  • frontend/src/pages/ConnectorsPage.jsx#L214-L224: handleConnectorSaved should branch on editingConnector (captured before it's cleared) the same way.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx` around
lines 171 - 184, Update ListOfTools.jsx at lines 171-184 in handleAddNewTool’s
success handler to call handleListRefresh() only for edits and call fetchPage(1,
pagination.pageSize, searchTerm) for creates. Update ToolSettings.jsx at line
140 in addNewItem to branch using editItemId captured before it is cleared,
refreshing edits and navigating to page 1 for creates. Update ConnectorsPage.jsx
at lines 214-224 in handleConnectorSaved to branch on editingConnector captured
before clearing it, using the same edit-refresh and create-page-1 behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/utils/tests/test_list_pagination.py`:
- Around line 163-198: Update test_client_ordering_keeps_pk_tiebreaker to create
at least two distinct modified_at groups with multiple rows tied within each
group. Build the expected names sorted by ascending modified_at and then
stringified pk, and keep the paginated ordering=modified_at request asserting
that the combined page results match this sequence.

In `@frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx`:
- Around line 90-111: The shared loading state is cleared before asynchronous
list refreshes finish. In
frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx lines
90-111, update handleDelete to remove the manual setIsLoading toggle and outer
finally, allowing handleListRefresh/fetchPage to own loading; in
frontend/src/components/workflows/workflow/Workflows.jsx lines 65-78, update
editProject so setLoading(false) is tied to the branch's completed work,
including the awaited handleListRefresh path, rather than the outer finally.

In `@frontend/src/components/workflows/workflow/Workflows.jsx`:
- Around line 65-78: Update the usePaginatedResource configuration in
Workflows.jsx so its onError handler reports failures through
setAlertDetails(handleException(...)), matching the error handling used by the
other converted list pages instead of only calling console.error. Preserve the
existing project-list request and loading behavior.

---

Outside diff comments:
In `@frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx`:
- Around line 171-184: Update ListOfTools.jsx at lines 171-184 in
handleAddNewTool’s success handler to call handleListRefresh() only for edits
and call fetchPage(1, pagination.pageSize, searchTerm) for creates. Update
ToolSettings.jsx at line 140 in addNewItem to branch using editItemId captured
before it is cleared, refreshing edits and navigating to page 1 for creates.
Update ConnectorsPage.jsx at lines 214-224 in handleConnectorSaved to branch on
editingConnector captured before clearing it, using the same edit-refresh and
create-page-1 behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eb979305-f23c-4372-9bd4-2c3913bdb440

📥 Commits

Reviewing files that changed from the base of the PR and between 45bf279 and c2ec0dc.

📒 Files selected for processing (9)
  • backend/backend/settings/base.py
  • backend/utils/filters/ordering_filter.py
  • backend/utils/tests/test_list_pagination.py
  • frontend/src/components/custom-tools/list-of-tools/ListOfTools.jsx
  • frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx
  • frontend/src/components/workflows/workflow/Workflows.jsx
  • frontend/src/hooks/usePaginatedResource.js
  • frontend/src/hooks/usePaginatedResource.test.js
  • frontend/src/pages/ConnectorsPage.jsx

Comment on lines +163 to +198
def test_client_ordering_keeps_pk_tiebreaker(self) -> None:
"""``?ordering=`` replaces the view default, so pk must still be appended.

Every row here shares one ``modified_at``. Primary keys are random
UUIDs, so ordering by pk is unrelated to insertion order — the returned
sequence only matches if pk survived as the tie-breaker.
"""
for endpoint in LIST_ENDPOINTS:
with self.subTest(kind=endpoint.kind):
created = []
for i in range(6):
obj = self._create(endpoint, f"{endpoint.kind}-tied-{i}")
self._stamp(obj, BASE_TIME)
created.append(obj)
expected = [
getattr(obj, endpoint.name_field)
for obj in sorted(created, key=lambda o: str(o.pk))
]

pages = [
self._list(
endpoint,
self.owner,
page=n,
page_size=3,
ordering="modified_at",
)
for n in (1, 2)
]
names = [
n
for page in pages
for n in self._names(endpoint, page.data["results"])
]

assert names == expected

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make client ordering distinguishable from default fallback.

All rows have the same modified_at, so accepted ordering=modified_at and fallback default -modified_at, pk produce the same PK sequence. Add at least two timestamp groups, with ties within each, and assert ascending modified_at, pk across pages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/utils/tests/test_list_pagination.py` around lines 163 - 198, Update
test_client_ordering_keeps_pk_tiebreaker to create at least two distinct
modified_at groups with multiple rows tied within each group. Build the expected
names sorted by ascending modified_at and then stringified pk, and keep the
paginated ordering=modified_at request asserting that the combined page results
match this sequence.

Comment on lines +90 to +111
const {
items: adapterList,
setItems: setAdapterList,
isLoading,
setIsLoading,
searchTerm,
setSearchTerm,
pagination,
fetchPage,
refresh: handleListRefresh,
handlePaginationChange,
handleSearch,
} = usePaginatedResource({
request: (params) =>
axiosPrivate({
method: "GET",
url: `/api/v1/unstract/${sessionDetails?.orgId}/adapter/`,
params: { ...params, adapter_type: type?.toUpperCase() },
}),
onError: (err) => setAlertDetails(handleException(err)),
defaultPageSize: DEFAULT_PAGE_SIZE,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Manually toggling the shared isLoading around an un-awaited handleListRefresh() races the refresh's own loading state. handleListRefresh() (→ fetchPage) sets isLoading true synchronously and then suspends on the network await; a caller's own .finally(() => setIsLoading(false)) on a sibling promise chain resolves within a few microtask ticks — well before that network call returns — so isLoading flips false while the refresh is still in flight, and the UI briefly shows the pre-action data with no loading indicator until the refreshed data snaps in.

  • frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx#L90-L111: handleDelete (unchanged, lines 144-158) wraps the delete call with setIsLoading(true) / .finally(() => setIsLoading(false)) while its .then() fires handleDeleteSuccess()handleListRefresh() without awaiting it; drop the manual toggle (or await the refresh) so fetchPage alone owns the flag:
axiosPrivate(requestOptions)
  .then(() => handleDeleteSuccess())
  .catch((err) => setAlertDetails(handleException(err)));
  • frontend/src/components/workflows/workflow/Workflows.jsx#L65-L78: editProject (unchanged, lines 104-131) has the same shape — its edit branch calls handleListRefresh() un-awaited inside a .then() that's wrapped by setLoading(true)/.finally(() => setLoading(false)); tie setLoading(false) to whichever branch actually runs instead of the outer .finally.
📍 Affects 2 files
  • frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx#L90-L111 (this comment)
  • frontend/src/components/workflows/workflow/Workflows.jsx#L65-L78
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx` around
lines 90 - 111, The shared loading state is cleared before asynchronous list
refreshes finish. In
frontend/src/components/tool-settings/tool-settings/ToolSettings.jsx lines
90-111, update handleDelete to remove the manual setIsLoading toggle and outer
finally, allowing handleListRefresh/fetchPage to own loading; in
frontend/src/components/workflows/workflow/Workflows.jsx lines 65-78, update
editProject so setLoading(false) is tied to the branch's completed work,
including the awaited handleListRefresh path, rather than the outer finally.

Comment on lines +65 to 78
items: projectList,
isLoading: loading,
setIsLoading: setLoading,
pagination,
setPagination,
searchTerm,
fetchPage,
refresh: handleListRefresh,
handlePaginationChange,
handleSearch,
} = usePaginatedList({
fetchData: (...args) => fetchListRef.current?.(...args),
} = usePaginatedResource({
request: (params) => projectApiService.getProjectList(params),
onError: () => console.error("Unable to get project list"),
defaultPageSize: DEFAULT_PAGE_SIZE,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Load failures are silently swallowed — no alert shown to the user.

Every other converted list page (ListOfTools.jsx, ToolSettings.jsx, ConnectorsPage.jsx) routes onError through setAlertDetails(handleException(...)). Here it only does console.error, so if the workflow list fails to load, the user sees an empty/loading page with no explanation.

🩹 Proposed fix
   } = usePaginatedResource({
     request: (params) => projectApiService.getProjectList(params),
-    onError: () => console.error("Unable to get project list"),
+    onError: (err) =>
+      setAlertDetails(handleException(err, "Unable to get project list")),
     defaultPageSize: DEFAULT_PAGE_SIZE,
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
items: projectList,
isLoading: loading,
setIsLoading: setLoading,
pagination,
setPagination,
searchTerm,
fetchPage,
refresh: handleListRefresh,
handlePaginationChange,
handleSearch,
} = usePaginatedList({
fetchData: (...args) => fetchListRef.current?.(...args),
} = usePaginatedResource({
request: (params) => projectApiService.getProjectList(params),
onError: () => console.error("Unable to get project list"),
defaultPageSize: DEFAULT_PAGE_SIZE,
});
items: projectList,
isLoading: loading,
setIsLoading: setLoading,
pagination,
searchTerm,
fetchPage,
refresh: handleListRefresh,
handlePaginationChange,
handleSearch,
} = usePaginatedResource({
request: (params) => projectApiService.getProjectList(params),
onError: (err) =>
setAlertDetails(handleException(err, "Unable to get project list")),
defaultPageSize: DEFAULT_PAGE_SIZE,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/workflows/workflow/Workflows.jsx` around lines 65 -
78, Update the usePaginatedResource configuration in Workflows.jsx so its
onError handler reports failures through setAlertDetails(handleException(...)),
matching the error handling used by the other converted list pages instead of
only calling console.error. Preserve the existing project-list request and
loading behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant