Skip to content

Conversation

@muhammad-ali-e
Copy link
Contributor

@muhammad-ali-e muhammad-ali-e commented Nov 24, 2025

What

  • Add global maximum execution count limit (default: 3) to prevent infinite processing loops
  • Add file history management UI with filtering, viewing, and bulk delete operations

Why

  • Users need visibility into which files have been processed and how many times
  • Users need to prevent infinite processing loops by enforcing maximum execution limits
  • Users need to clean up file history to manage storage and maintain data hygiene
  • Users need granular control over file history with filtering by status and execution count

How

Backend (Django):

  • Created FileHistoryViewSet with read-only endpoints for listing, retrieving, deleting individual and bulk clearing file history records
  • Created IsWorkflowOwnerOrShared permission class to ensure only workflow owners and shared users can access file history
  • Added database migration (0018) to add execution_count field to FileHistory model and max_file_execution_count field to Workflow model (prepared for future workflow-level limits)
  • Enhanced FileHistory model with execution count tracking and limit checking logic
  • Added global default maximum execution count (currently set to 3, configurable via settings)
  • Enhanced Workflow model with get_max_execution_count() method supporting configuration hierarchy (prepared for future workflow > org > global expansion)
  • Updated FileHistorySerializer to include computed fields for max execution count and limit exceeded status
  • Added new URL routes under /workflows/<id>/file-histories/ for CRUD operations
  • Updated internal views to support file history operations

Frontend (React):

  • Created FileHistoryModal component with Ant Design table, pagination, and multi-select row selection
  • Added filter panel supporting status filter (ERROR, COMPLETED) and execution count range filters (min/max)
  • Added action buttons for delete selected, clear all matching filters, and refresh data
  • Integrated modal into Pipelines component with "View File History" menu item
  • Added API service methods in workflow-service.js for file history operations

Workers:

  • Updated filter pipeline to check execution count limits before processing
  • Updated workflow execution service to increment execution count and handle limit exceeded scenarios
  • Updated core data models to support execution count tracking

Future Expansion:

  • Database schema includes max_file_execution_count field on Workflow model for future workflow-level limit configuration
  • Architecture supports extending limits to organization level and workflow level in future releases

Can this PR break any existing features. If yes, please list possible items. If no, please explain why.

  • No breaking changes. This PR is backward compatible because:
    • Database migration adds new fields with default values (execution_count defaults to 0, max_file_execution_count is nullable)
    • Existing file history records are preserved and will work with the new UI
    • The feature is additive - existing workflows continue to function without needing configuration changes
    • Global default limit (3) is enforced for all workflows, preventing infinite loops while allowing reasonable retries
    • Permission checks use existing organization-scoped access patterns
    • Workers gracefully handle missing execution count data

Database Migrations

  • Migration 0018: backend/workflow_manager/workflow_v2/migrations/0018_filehistory_execution_count_and_more.py
    • Adds execution_count field to FileHistory model (IntegerField, default=0)
    • Adds max_file_execution_count field to Workflow model (IntegerField, nullable, prepared for future workflow-level configuration)
    • Adds database indexes on FileHistory.status and FileHistory.execution_count for efficient filtering

Env Config

  • No new environment variables required
  • Global default max execution count is currently hardcoded to 3 (can be made configurable in future releases)

Relevant Docs

  • File history management API endpoints documentation should be added to API reference
  • User guide for file history viewer should be added to product documentation
  • Document global execution limit (default: 3) and future expansion to org/workflow levels

Related Issues or PRs

  • Jira ticket: UN-3011
  • Jira ticket: UN-3028
  • Jira ticket: UN-3029

Dependencies Versions

  • No new dependencies added
  • Uses existing Ant Design components (Table, Modal, Button, Select, InputNumber)
  • Uses existing Django REST Framework for API endpoints

Notes on Testing

Backend Testing:

  • Test file history list endpoint with pagination
  • Test filtering by status (ERROR, COMPLETED) and execution count range
  • Test delete single file history record
  • Test bulk clear with filters
  • Test permission checks for workflow owner, shared users, and non-authorized users
  • Test execution count increment logic in workers
  • Test global max execution count limit enforcement (default: 3)
  • Test Redis cache clearing on file history deletion
  • Verify workflow-level max_file_execution_count field exists but is not enforced yet (prepared for future)

Frontend Testing:

  • Test file history modal opens from pipelines action menu
  • Test table displays file path, status, execution count, last run, error, and actions
  • Test pagination (10 items per page)
  • Test multi-select row selection
  • Test delete selected files action
  • Test clear all action with confirmation dialog
  • Test filter panel (status dropdown, min/max execution count inputs)
  • Test copy-to-clipboard for file paths and error messages
  • Test refresh data action
  • Test responsive design on mobile, tablet, and desktop

Integration Testing:

  • Test end-to-end workflow: file upload → processing → execution count increment → view in file history
  • Test limit enforcement: file reaches max execution count (3) → processing stops → status shown in UI
  • Test bulk delete: select multiple files → delete → verify removed from database and cache
  • Test behavior when file hits global limit: verify error message, verify file not reprocessed

Screenshots

Screenshot from 2025-12-01 12-23-46

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

…iewer

- Add file history management UI with bulk operations
- Add execution count tracking and limits
- Add permissions for workflow-scoped access

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Summary by CodeRabbit

Release Notes

  • New Features
    • Added file execution limits to prevent redundant processing; configure per workflow or globally via environment variable.
    • Introduced File History management interface with filtering by status, execution count, and file path.
    • Enabled bulk operations to delete or clear multiple file history records efficiently.
    • Improved result caching to only use cached data when execution is complete.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Adds execution-count tracking and limits for file processing: new config, DB fields/indexes, atomic file-history creation with race handling, APIs/UI to view/manage histories, permission checks, worker logic to honor limits, and caching changes to only reuse COMPLETED results.

Changes

Cohort / File(s) Summary
Configuration & Env
backend/backend/settings/base.py, backend/sample.env
Add MAX_FILE_EXECUTION_COUNT config (env var, default 3).
Models & Migration
backend/workflow_manager/workflow_v2/models/file_history.py, backend/workflow_manager/workflow_v2/models/workflow.py, backend/workflow_manager/workflow_v2/migrations/0018_filehistory_execution_count_and_more.py
Add execution_count to FileHistory, max_file_execution_count to Workflow/ExecutionAction, add indexes (status, execution_count, file_path, trigram), add __str__, has_exceeded_limit, and get_max_execution_count helpers.
FileHistory Helper
backend/workflow_manager/workflow_v2/file_history_helper.py
Add atomic _increment_file_history, safe-string helpers, IntegrityError race recovery, and update create_file_history to return record with execution_count.
API Views & URLs
backend/workflow_manager/workflow_v2/file_history_views.py, backend/workflow_manager/workflow_v2/urls/workflow.py
New FileHistoryViewSet (list/retrieve/destroy/clear) with filters (status, execution_count_min/max, file_path) and nested routes for list/detail/clear.
Internal Views & Batch Responses
backend/workflow_manager/internal_views.py, backend/workflow_manager/workflow_v2/views.py
Thread execution_count and max_execution_count into batch responses/logs; reuse precomputed serializer data in batch lookups.
Permissions & Serialization
backend/workflow_manager/workflow_v2/permissions.py, backend/workflow_manager/workflow_v2/serializers.py
New IsWorkflowOwnerOrShared permission (caches workflow on request); extend FileHistorySerializer with max_execution_count and has_exceeded_limit.
Worker Processing & API Deployment
workers/shared/processing/filter_pipeline.py, workers/shared/workflow/execution/service.py, workers/api-deployment/tasks.py, workers/shared/processing/files/processor.py
Enforce execution-count-based skips (use has_exceeded_limit / execution_count vs max), simplify status-based acceptance, only reuse cached results when status is COMPLETED, and propagate status/error into FileHistory creation.
Frontend UI & Services
frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx, frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.css, frontend/src/components/pipelines-or-deployments/pipelines/Pipelines.jsx, frontend/src/components/workflows/workflow/workflow-service.js, frontend/src/components/agency/agency/Agency.jsx
Add File History modal (filters, pagination, copy, delete, bulk clear) with CSS; integrate modal into Pipelines and Agency UIs; add API service methods getFileHistories, deleteFileHistory, bulkClearFileHistories, bulkDeleteFileHistoriesByIds.
Client Data Model
unstract/core/src/unstract/core/data_models.py
Add execution_count to FileHistoryData and map it in from_dict.
Frontend Styling
frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.css
New CSS for the File History modal (layout, responsiveness).

Sequence Diagram(s)

sequenceDiagram
    participant FE as Frontend
    participant API as Django API
    participant Helper as FileHistoryHelper
    participant DB as Postgres
    participant Cache as Redis

    FE->>API: POST /workflow/{id}/file-histories/ (create)
    API->>Helper: create_file_history(workflow, file_hash, status, result, metadata, error, is_api)
    Helper->>DB: SELECT FileHistory WHERE workflow & file_hash
    alt exists
        Helper->>DB: UPDATE execution_count = F('execution_count') + 1, set status/result/error
        DB-->>Helper: updated record
        Helper->>Cache: invalidate per-file cache
        Helper-->>API: return updated FileHistory (execution_count++)
    else not exists
        Helper->>DB: INSERT FileHistory (execution_count=1)
        alt IntegrityError (race)
            Helper->>DB: SELECT & UPDATE execution_count = F('execution_count') + 1
            Helper->>Cache: invalidate per-file cache
            Helper-->>API: return existing+incremented FileHistory
        else success
            Helper-->>API: return new FileHistory (execution_count=1)
        end
    end
    API-->>FE: Response {file_history_id, execution_count, ...}
Loading
sequenceDiagram
    participant Worker as Worker
    participant Pipeline as filter_pipeline
    participant API as Django API
    participant DB as Postgres

    Worker->>Pipeline: evaluate file for processing
    Pipeline->>API: request file history info (status, execution_count, max_execution_count)
    API->>DB: fetch FileHistory and Workflow.max_file_execution_count
    DB-->>API: return file history + workflow limit
    API-->>Pipeline: return history data
    alt has_exceeded_limit true OR execution_count >= max
        Pipeline-->>Worker: SKIP (limit reached)
    else status allows reprocess
        Pipeline-->>Worker: ACCEPT for processing
    else status indicates skip
        Pipeline->>Pipeline: compare history_path vs current_path
        alt same path
            Pipeline-->>Worker: SKIP (already processed)
        else
            Pipeline-->>Worker: ACCEPT (new path)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Pay extra attention to:
    • backend/workflow_manager/workflow_v2/file_history_helper.py — atomic increments, IntegrityError recovery, cache invalidation.
    • workers/shared/processing/filter_pipeline.py — revised SKIP/ACCEPT logic and backward-compatible gating.
    • backend/workflow_manager/workflow_v2/file_history_views.py — bulk clear/destroy validation and limits.
    • workers/shared/workflow/execution/service.py & workers/api-deployment/tasks.py — status/error propagation and caching gating.
    • frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx — filter state, pagination, bulk actions, and API error handling.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main feature: limiting maximum file execution count and adding a file history viewer. It is specific, concise, and directly relates to the primary changes in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 84.85% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed PR description comprehensively covers all template sections including What, Why, How, breaking changes assessment, migrations, environment config, related issues, testing notes, and checklists.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/UN-3011-FEAT_limit_maximum_file_execution_count_with_file_history_viewer

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (2)
workers/shared/processing/filter_pipeline.py (1)

388-445: Execution-count check may over-enforce limits (especially for API workflows) and is inconsistent with individual path

The new logic in _evaluate_batch_file_history:

  • Unconditionally skips when execution_count >= max_execution_count, before considering status or path.
  • Does not use the backend’s has_exceeded_limit flag, even though FileHistorySerializer now exposes it.
  • Has no counterpart in _evaluate_file_history, so behavior differs between batch (get_files_history_batch) and individual (get_file_history) flows.

Two concrete risks:

  1. API workflows
    On the backend, FileHistory.has_exceeded_limit() explicitly returns False for API deployments, so limits are not meant to be enforced for them. However, the worker’s batch filter now enforces a hard cap purely from execution_count/max_execution_count, so API-type workflows could start being skipped once the numeric threshold is reached, contradicting backend semantics.

  2. Inconsistent semantics between batch and fallback paths
    If the batch API fails and _process_file_history_individual is used, _evaluate_file_history never applies the execution-count check, so the max-count limit “disappears” in that mode.

A more robust approach would be to:

  • Prefer the backend’s boolean flag when present, and only fall back to raw counts for backward compatibility:
-            execution_count = file_history.get("execution_count", 0)
-            max_execution_count = file_history.get("max_execution_count", 3)
+            has_exceeded_limit = file_history.get("has_exceeded_limit")
+            execution_count = file_history.get("execution_count", 0)
+            max_execution_count = file_history.get("max_execution_count")
...
-            if execution_count >= max_execution_count:
+            if has_exceeded_limit is True:
+                logger.warning(
+                    f"FileHistoryFilter - {file_hash.file_name}: SKIP "
+                    f"(backend reports has_exceeded_limit=True)"
+                )
+                return True
+            if (
+                has_exceeded_limit is None
+                and max_execution_count is not None
+                and execution_count >= max_execution_count
+            ):
                 logger.warning(
                     f"FileHistoryFilter - {file_hash.file_name}: SKIP "
-                    f"(max execution count reached: {execution_count}/{max_execution_count})"
+                    f"(max execution count reached: {execution_count}/{max_execution_count})"
                 )
                 return True
  • Align _evaluate_file_history (individual path) to apply the same has_exceeded_limit flag once get_file_history_internal starts returning it, so both code paths enforce limits identically.

This keeps the worker honoring backend rules (including “no limit for API workflows”) and avoids surprising discrepancies between batch and fallback behavior.

workers/shared/workflow/execution/service.py (1)

1448-1496: Reset _last_execution_error per execution to avoid stale ERROR statuses in file history.

The new logic for:

  • Deriving file_status/error_message from processing_error or _last_execution_error, and
  • Always creating file history entries (with correct COMPLETED/ERROR) via _should_create_file_history

is conceptually correct and matches the intended execution-count tracking.

However, _last_execution_error is only initialized in __init__ and is never explicitly reset at the beginning of execute_workflow_for_file. If this service instance is reused across files, a previous failure can leave _last_execution_error populated and cause:

  • file_status to be set to ERROR even when the current run succeeded, and
  • error_message / final FinalOutputResult.error to contain an old error.

That now directly affects stored FileHistory.status and error.

Consider explicitly clearing _last_execution_error at the start of execute_workflow_for_file (or just before starting a new workflow execution), for example:

def execute_workflow_for_file(
    self,
    file_processing_context: FileProcessingContext,
    organization_id: str,
    workflow_id: str,
    execution_id: str,
    is_api: bool = False,
    use_file_history: bool = False,
    workflow_file_execution_id: str = None,
    workflow_logger=None,
) -> WorkflowExecutionResult:
-    """Execute workflow with clean, linear flow and comprehensive result propagation."""
+    """Execute workflow with clean, linear flow and comprehensive result propagation."""
+    # Reset last execution error for this run to avoid leaking state between files
+    self._last_execution_error = None

This keeps your new status/error propagation for file history accurate per run.

Also applies to: 1511-1530

🧹 Nitpick comments (13)
backend/workflow_manager/internal_views.py (2)

505-506: Consider downgrading the extra status log to debug to reduce noise

You now log status updates at Line 473, Line 505, and Line 517. The new Line 505 log is redundant at INFO level; consider switching it to DEBUG to avoid triple INFO logs per status change:

-logger.info(f"Updating status for execution {id} to {status_enum}")
+logger.debug(f"Updating status for execution {id} to {status_enum}")

2309-2352: File history batch/create responses now carry execution limits; update docstrings for accuracy

The additions to FileHistoryBatchCheckView and FileHistoryCreateView correctly:

  • Fetch workflow.get_max_execution_count() once and attach it per record and at the top level.
  • Expose execution_count and status alongside existing fields.
  • Return execution_count from the create endpoint, which is useful for worker-side decisions.

The only follow‑up is to update the docstrings/examples for these endpoints so they describe the enriched response shapes (including execution_count, status, max_execution_count) to keep internal API documentation accurate.

Also applies to: 2483-2494

frontend/src/components/pipelines-or-deployments/pipelines/Pipelines.jsx (1)

474-502: View File History action and modal wiring look good; consider small cleanups

Functionally this integration is solid: you guard on selectedPorD?.workflow_id before opening the modal and surface a clear error if it’s missing, and the modal gets the right identifiers.

Two small polish suggestions:

  • Drop or downgrade the console.log("Opening File History for:", …) in the action handler to avoid noisy logs in production.
  • FileHistoryModal’s workflowName prop is currently fed selectedPorD?.pipeline_name, so the title “File History - …” will show the pipeline name, not the underlying workflow name. If the intent is to show the workflow label, consider passing selectedPorD?.workflow_name instead, or rename the prop to something like title/pipelineName for clarity.

Also applies to: 785-790

backend/workflow_manager/workflow_v2/views.py (1)

1073-1094: Avoid re-serializing the same FileHistory for multiple identifiers

Inside the for result_identifier in matched_identifiers loop you build FileHistorySerializer(fh) and serializer.data on every iteration, even though fh is the same for all identifiers. This is minor but unnecessary work.

You can serialize once per fh and reuse the dict:

-        for result_identifier in matched_identifiers:
-            is_completed_result = fh.is_completed()
-            serializer = FileHistorySerializer(fh)
-            file_history_data = serializer.data
+        is_completed_result = fh.is_completed()
+        serializer = FileHistorySerializer(fh)
+        file_history_data = serializer.data
+
+        for result_identifier in matched_identifiers:
             ...
             response_data[result_identifier] = {
                 "found": True,
                 "is_completed": is_completed_result,
                 "file_history": file_history_data,
             }

This keeps the new logging/fields but avoids redundant serialization work.

backend/workflow_manager/workflow_v2/permissions.py (1)

20-45: Make permission more robust to kwarg naming and unauthenticated users

has_permission currently assumes that all views using this permission expose the workflow id as view.kwargs["workflow_id"]. If any route uses a different kwarg (e.g. pk or workflow_pk), this will silently deny access for all requests.

Also, user is used without an explicit is_authenticated guard; if this permission is ever applied without an authentication gate, accessing user.organization could be fragile.

Consider:

  • Either asserting/standardizing that all relevant routes use workflow_id, or reading from a small set of accepted keys:
workflow_id = (
    view.kwargs.get("workflow_id")
    or view.kwargs.get("workflow_pk")
    or view.kwargs.get("pk")
)
if not workflow_id:
    return False
  • Optionally short‑circuiting unauthenticated users:
user = request.user
if not getattr(user, "is_authenticated", False):
    return False

This keeps the caching behavior but makes the permission less sensitive to routing details.

backend/workflow_manager/workflow_v2/models/file_history.py (1)

23-45: Execution limit modelling looks consistent; minor optional tightening

The new __str__, execution_count field, and has_exceeded_limit logic line up well with the workflow-level get_max_execution_count() and the “no limit for API workflows” rule.

If you want to tighten the model further, consider:

  • Using PositiveIntegerField (or at least validators=[MinValueValidator(0)]) for execution_count to prevent negative values.
  • Documenting explicitly in the docstring whether execution_count is “completed runs so far” vs “including current in‑flight run”, to keep helpers and workers aligned on when they increment.

Functionally, though, this change looks sound.

Also applies to: 65-68

backend/workflow_manager/workflow_v2/serializers.py (1)

133-162: Serializer methods are fine; watch for N+1 on workflow access

The added max_execution_count / has_exceeded_limit methods correctly defer to Workflow.get_max_execution_count() and FileHistory.has_exceeded_limit().

One thing to double‑check is that any views returning lists of FileHistory use select_related("workflow") so these per-row method calls don’t introduce an N+1 query pattern when serializing many histories. In file_history_batch_lookup_internal you already do this; mirroring that in other entry points would keep performance predictable.

backend/workflow_manager/workflow_v2/file_history_helper.py (1)

271-359: Execution count increment + race handling look correct; consider tightening the try/except and logging.

The new flow for existing histories (atomic F("execution_count") + 1) and the IntegrityError race-resolution path both look solid for concurrent workers and align with the unique constraints on FileHistory.

Two non-blocking refinements:

  • Narrow the try block around FileHistory.objects.create(**create_data) so that only the actual create is treated as a “race condition” source; this avoids accidentally classifying unrelated IntegrityErrors as duplicate races.
  • In the rare existing_record is None branch, prefer logger.exception(...) to retain the original traceback before re‑raising, and optionally use f"... Error: {e!s}" instead of str(e) for cleaner formatting.

These are style/diagnostics improvements only; behavior is otherwise correct.

frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (3)

68-137: Unify error handling for fetching histories with useExceptionHandler and drop console noise.

The fetch logic and query param construction look correct, but two small cleanups will make this more consistent with the rest of the app:

  • Instead of manually unpacking err.response.data and building errorMessage, reuse useExceptionHandler so all errors (validation, subscription, network) are handled uniformly:
-    } catch (err) {
-      console.error("Error fetching file histories:", err);
-      console.error("Error details:", {
-        message: err?.message,
-        response: err?.response,
-        status: err?.response?.status,
-        data: err?.response?.data,
-      });
-
-      // Extract more detailed error message
-      const errorMessage =
-        err?.response?.data?.message ||
-        err?.response?.data?.error ||
-        err?.response?.data?.detail ||
-        err?.message ||
-        "Failed to fetch file histories";
-
-      setAlertDetails({
-        type: "error",
-        content: errorMessage,
-      });
-    } finally {
+    } catch (err) {
+      setAlertDetails(
+        handleException(
+          err,
+          "Failed to fetch file histories"
+        )
+      );
+    } finally {
  • Remove or gate the console.log calls used for debugging ("Fetching file histories:", "File histories response:") so they don’t spam production logs.

These are behavioral no-ops but improve consistency and cleanliness.


153-167: Filter reset comment is misleading; logic relies on the explicit refetch, not an effect.

The reset handler works by clearing filter state and then explicitly refetching:

setStatusFilter([]);
setExecutionCountMin(null);
setExecutionCountMax(null);
// Reset will trigger effect to fetch unfiltered data
setTimeout(() => {
  fetchFileHistories(1, pagination.pageSize);
}, 0);

The comment suggests that a React effect will fire on filter change, but useEffect currently only depends on [open, workflowId]. Consider updating the comment (or just removing the setTimeout and calling fetchFileHistories directly) to avoid confusion for future maintainers.


169-211: Batch delete behavior is correct but can be slightly more informative on partial failures.

The handleDeleteSelected implementation correctly:

  • Short-circuits when nothing is selected.
  • Deletes all selected IDs via Promise.all.
  • Refreshes and clears selection, with a success alert.

If you want to harden it further, consider catching per‑ID failures and reporting how many deletions actually succeeded vs. failed instead of treating the whole batch as a single success/failure. Not required for correctness, but improves UX for large selections.

backend/workflow_manager/workflow_v2/file_history_views.py (2)

19-25: Consider adding type annotations for class attributes.

While the current implementation works correctly, adding ClassVar annotations would improve type safety and make the intent clearer.

Apply this diff to add type annotations:

+from typing import ClassVar
+
 class FileHistoryViewSet(viewsets.ReadOnlyModelViewSet):
     """ViewSet for file history operations with filtering support."""
 
-    serializer_class = FileHistorySerializer
-    lookup_field = "id"
-    permission_classes = [IsAuthenticated, IsWorkflowOwnerOrShared]
-    pagination_class = CustomPagination
+    serializer_class: ClassVar = FileHistorySerializer
+    lookup_field: ClassVar[str] = "id"
+    permission_classes: ClassVar = [IsAuthenticated, IsWorkflowOwnerOrShared]
+    pagination_class: ClassVar = CustomPagination

55-69: Consider using 204 No Content for DELETE operations.

The method returns 200 OK with a message body. While functional, RESTful conventions typically use 204 No Content for successful DELETE operations with no response body.

Apply this diff to use standard REST response:

 return Response(
-    {"message": "File history deleted successfully"}, status=status.HTTP_200_OK
+    status=status.HTTP_204_NO_CONTENT
 )

Note: The static analysis warnings about unused request and id parameters are false positives. These parameters are required by DRF's routing and self.get_object() internally uses them.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between d00f9e6 and 127f129.

📒 Files selected for processing (19)
  • backend/backend/settings/base.py (1 hunks)
  • backend/sample.env (1 hunks)
  • backend/workflow_manager/internal_views.py (5 hunks)
  • backend/workflow_manager/workflow_v2/file_history_helper.py (5 hunks)
  • backend/workflow_manager/workflow_v2/file_history_views.py (1 hunks)
  • backend/workflow_manager/workflow_v2/migrations/0018_filehistory_execution_count_and_more.py (1 hunks)
  • backend/workflow_manager/workflow_v2/models/file_history.py (2 hunks)
  • backend/workflow_manager/workflow_v2/models/workflow.py (3 hunks)
  • backend/workflow_manager/workflow_v2/permissions.py (1 hunks)
  • backend/workflow_manager/workflow_v2/serializers.py (1 hunks)
  • backend/workflow_manager/workflow_v2/urls/workflow.py (3 hunks)
  • backend/workflow_manager/workflow_v2/views.py (1 hunks)
  • frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.css (1 hunks)
  • frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (1 hunks)
  • frontend/src/components/pipelines-or-deployments/pipelines/Pipelines.jsx (5 hunks)
  • frontend/src/components/workflows/workflow/workflow-service.js (1 hunks)
  • unstract/core/src/unstract/core/data_models.py (2 hunks)
  • workers/shared/processing/filter_pipeline.py (1 hunks)
  • workers/shared/workflow/execution/service.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (12)
frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (1)
frontend/src/hooks/useExceptionHandler.jsx (1)
  • useExceptionHandler (4-127)
backend/workflow_manager/workflow_v2/views.py (1)
backend/workflow_manager/workflow_v2/serializers.py (1)
  • FileHistorySerializer (133-161)
backend/workflow_manager/workflow_v2/serializers.py (2)
backend/workflow_manager/workflow_v2/models/file_history.py (1)
  • has_exceeded_limit (27-44)
backend/workflow_manager/workflow_v2/models/workflow.py (1)
  • get_max_execution_count (107-126)
backend/workflow_manager/workflow_v2/file_history_helper.py (1)
backend/workflow_manager/workflow_v2/models/file_history.py (2)
  • FileHistory (14-133)
  • update (115-133)
workers/shared/processing/filter_pipeline.py (1)
unstract/core/src/unstract/core/data_models.py (1)
  • ExecutionStatus (336-369)
backend/workflow_manager/workflow_v2/permissions.py (2)
backend/workflow_manager/workflow_v2/models/workflow.py (1)
  • Workflow (33-137)
backend/workflow_manager/internal_views.py (6)
  • get (628-688)
  • get (1188-1298)
  • get (1603-1656)
  • get (1851-1948)
  • get (2159-2242)
  • get (2513-2616)
backend/workflow_manager/workflow_v2/models/file_history.py (2)
backend/workflow_manager/workflow_v2/models/workflow.py (3)
  • Workflow (33-137)
  • WorkflowType (34-39)
  • get_max_execution_count (107-126)
backend/workflow_manager/workflow_v2/serializers.py (1)
  • get_max_execution_count (141-150)
backend/workflow_manager/workflow_v2/models/workflow.py (1)
backend/workflow_manager/workflow_v2/serializers.py (1)
  • get_max_execution_count (141-150)
backend/workflow_manager/internal_views.py (2)
backend/workflow_manager/workflow_v2/models/workflow.py (1)
  • get_max_execution_count (107-126)
backend/workflow_manager/workflow_v2/serializers.py (1)
  • get_max_execution_count (141-150)
frontend/src/components/pipelines-or-deployments/pipelines/Pipelines.jsx (1)
frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (1)
  • FileHistoryModal (32-533)
backend/workflow_manager/workflow_v2/urls/workflow.py (1)
backend/workflow_manager/workflow_v2/file_history_views.py (1)
  • FileHistoryViewSet (19-135)
workers/shared/workflow/execution/service.py (2)
unstract/core/src/unstract/core/data_models.py (1)
  • ExecutionStatus (336-369)
unstract/core/src/unstract/core/worker_models.py (3)
  • has_error (302-304)
  • has_error (760-762)
  • has_error (820-822)
🪛 GitHub Check: SonarCloud Code Analysis
backend/workflow_manager/workflow_v2/models/workflow.py

[warning] 119-119: Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=Zipstack_unstract&issues=AZq0mug6l4UGbU48qy8L&open=AZq0mug6l4UGbU48qy8L&pullRequest=1676

🪛 Ruff (0.14.5)
backend/workflow_manager/workflow_v2/file_history_views.py

24-24: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


55-55: Unused method argument: request

(ARG002)


55-55: Unused method argument: id

(ARG002)


122-122: Do not catch blind exception: Exception

(BLE001)


134-134: Do not catch blind exception: Exception

(BLE001)

backend/workflow_manager/workflow_v2/file_history_helper.py

319-319: Consider moving this statement to an else block

(TRY300)


327-327: Use explicit conversion flag

Replace with conversion flag

(RUF010)


355-358: Use logging.exception instead of logging.error

Replace with exception

(TRY400)

backend/workflow_manager/workflow_v2/migrations/0018_filehistory_execution_count_and_more.py

7-9: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


11-42: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (11)
backend/backend/settings/base.py (1)

203-204: Env-backed MAX_FILE_EXECUTION_COUNT constant is wired correctly

Reading MAX_FILE_EXECUTION_COUNT from the environment with an integer default of 3 is consistent with other settings and aligns with the new sample.env entry; no issues from a configuration or type perspective.

unstract/core/src/unstract/core/data_models.py (1)

1178-1184: FileHistoryData.execution_count wiring matches backend defaults

Adding execution_count with a default of 1 and mapping it in from_dict keeps this shared DTO in sync with the Django migration default and the helper logic that increments counts. Missing values will sensibly fall back to 1, so this remains backward compatible.

Also applies to: 1203-1212

backend/sample.env (1)

220-223: Sample MAX_FILE_EXECUTION_COUNT env is consistent with backend configuration

The new MAX_FILE_EXECUTION_COUNT entry is clearly documented (default 3, ETL/TASK scope) and matches the Django setting name and default; nothing else needed.

backend/workflow_manager/workflow_v2/migrations/0018_filehistory_execution_count_and_more.py (1)

1-42: Migration correctly adds execution_count/max_file_execution_count and supporting indexes

The new fields and defaults line up with the model and shared DTO changes, and the indexes on (workflow, status) and (workflow, execution_count) are appropriate for the expected lookup patterns. This migration looks ready to ship as-is.

frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.css (1)

1-90: FileHistoryModal styles are well-scoped and responsive

CSS is correctly namespaced under .file-history-modal, with sensible spacing and responsive tweaks for mobile/tablet/desktop breakpoints; there are no apparent clashes or layout issues.

frontend/src/components/workflows/workflow/workflow-service.js (1)

71-100: File history service endpoints are consistent with backend routes and CSRF usage.

The three helpers correctly map to the new DRF endpoints, pass query params for listing, and send CSRF-protected DELETE/POST requests for mutating operations. Shapes (params vs data) also align with the described backend filters. No changes needed here.

backend/workflow_manager/workflow_v2/urls/workflow.py (1)

29-33: File history URL wiring matches the viewset and frontend usage.

The new FileHistoryViewSet bindings and nested routes (workflow_id + id) align with the viewset’s lookup_field="id" and the frontend service paths. Naming and scoping look good; I don’t see any routing or parameter issues.

Also applies to: 79-94

frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (2)

260-383: Table columns and execution limit display align with backend fields.

The table configuration correctly:

  • Uses id as rowKey.
  • Renders execution_count alongside max_execution_count and has_exceeded_limit, highlighting exceeded entries in red.
  • Maps known statuses to AntD tag colors and falls back gracefully for unknown ones.
  • Provides copy‑to‑clipboard for file paths and error messages with proper event stopping.

This matches the serializer contract (execution_count, max_execution_count, has_exceeded_limit, modified_at, error) and looks good.


384-532: Modal behavior and actions are coherent with the workflow service and UX.

The modal wiring (open/close, filters, bulk actions, responsive layout, scrollable table) works cohesively:

  • Disables destructive actions when no rows are selected.
  • Correctly calls bulkClearFileHistories with the same filters used for listing.
  • Keeps pagination and selection in sync with refreshes.
  • Uses PropTypes to document expected props.

No blocking issues here; behavior is consistent with the introduced backend APIs.

backend/workflow_manager/workflow_v2/models/workflow.py (2)

4-4: LGTM!

The import is necessary for accessing settings.MAX_FILE_EXECUTION_COUNT in the get_max_execution_count() method.


107-127: No action required.

The settings.MAX_FILE_EXECUTION_COUNT is properly defined in backend/backend/settings/base.py:204 with a sensible default value of 3. The method is safe and will not raise an AttributeError at runtime.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
workers/api-deployment/tasks.py (1)

1128-1133: Fix inconsistent return type when no file hashes are available

Here the function returns only hash_values_of_files, but the signature and all other exit paths return a (files_to_process, cached_results) tuple, and the caller unpacks two values. This will raise a ValueError: not enough values to unpack whenever this branch is hit (e.g., when no hashes are present but use_file_history is enabled).

Recommend returning an empty cached-results dict for consistency:

-            logger.info(
-                f"No file hashes available for history check in execution {execution_id}, processing all files"
-            )
-            return hash_values_of_files
+            logger.info(
+                f"No file hashes available for history check in execution {execution_id}, processing all files"
+            )
+            return hash_values_of_files, {}
🧹 Nitpick comments (7)
workers/api-deployment/tasks.py (1)

1168-1195: Status-gated caching logic is solid; consider small cleanups and confirm limit semantics

The new logic only treats histories with status == ExecutionStatus.COMPLETED.value as cacheable and routes ERROR/STOPPED/other statuses back into files_to_process, which aligns with the docstring (“successful results are returned as cached”) and avoids reusing failed executions. That behavior looks correct.

Two follow-ups:

  • skipped_files is populated but never used; either log a summary with it or remove it to avoid confusion.
  • With the new global max-execution-count feature, please confirm that check_file_history_batch already encodes “limit exceeded” behavior appropriately (e.g., by not including those hashes in processed_file_hashes or by adjusting status/flags) so this function doesn’t accidentally keep re-queuing files that should be hard-stopped by the limit.
backend/workflow_manager/workflow_v2/file_history_helper.py (1)

306-404: Race-safe increment path looks good; TRY300 linter suggestion is optional

The new create_file_history flow correctly:

  • Resolves file_path based on is_api.
  • Reuses existing histories via _increment_file_history with F("execution_count") + 1.
  • Handles uniqueness races by catching IntegrityError, refetching, and incrementing using the same atomic helper.

Behaviorally this gives you N increments under N concurrent creators without lost updates.

Ruff’s TRY300 hint about using a try/except/else here is stylistic; if you want to silence it, you can move the “happy path” logging/return into an else: block, but that’s not functionally required.

frontend/src/components/agency/agency/Agency.jsx (1)

45-46: File history modal integration looks consistent; consider guarding on missing workflow id

The wiring for FileHistoryModal (state, menu item, click handler, and render) is consistent with the Pipelines integration and respects existing loading/clearing disables.

One small optional hardening: if there’s any path where details?.id might be unset when the page becomes interactive, you could either:

  • Disable the “View File History” menu item until details?.id is available, or
  • Early-return in handleMenuClick when !details?.id before opening the modal.

If the store invariant guarantees details.id is always present post‑load, this is fine as is.

Also applies to: 100-101, 1007-1030, 1333-1339

workers/shared/processing/files/processor.py (1)

102-117: Only caching COMPLETED histories is a good safety gate; inner import can be removed

Limiting cache reuse to status == ExecutionStatus.COMPLETED.value is a sensible tightening: it prevents serving partial, pending, or error results from history and cleanly falls back to fresh execution otherwise.

Minor cleanup: this function re-imports ExecutionStatus even though it’s already imported at the top of the module. You can safely drop the in-function import and rely on the module-level one to avoid redundant imports:

-                status = file_history_data.get("status")
-
-                from unstract.core.data_models import ExecutionStatus
-
-                if status != ExecutionStatus.COMPLETED.value:
+                status = file_history_data.get("status")
+                if status != ExecutionStatus.COMPLETED.value:
                     ...
workers/shared/processing/filter_pipeline.py (1)

393-423: Execution-count limit integration is correct; note the fallback default coupling

The new _evaluate_batch_file_history logic looks consistent with the backend model:

  • It prefers the backend-computed has_exceeded_limit flag, so workflow-type nuances (API vs ETL/TASK) stay centralized in the backend.
  • When has_exceeded_limit is None, it falls back to a raw execution_count >= max_execution_count check, which is a sensible compatibility path.
  • Status/path handling remains as before: only EXECUTING/PENDING/COMPLETED statuses participate in the “skip if same path” rule; other statuses (e.g., ERROR) allow reprocessing until the limit is hit.

Two minor considerations:

  1. Fallback default of 3
    The local default max_execution_count = file_history.get("max_execution_count", 3) is only used when older backends don’t send max_execution_count or has_exceeded_limit, but it does implicitly couple worker behavior to the historical global default. If that default ever changes for legacy servers, remember to update this fallback to stay consistent.

  2. Clarify intent in comments
    The comments already mention backend precedence; you might add a brief note that completed runs are still skipped based on status/path, and the limit primarily caps repeated non-terminal/error runs.

Functionally, the limit logic and logging look solid.

Also applies to: 428-459

frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (1)

169-178: Consider removing setTimeout in favor of immediate fetch.

The setTimeout(..., 0) on lines 175-177 defers the fetch to allow state updates to complete. Since fetchFileHistories reads from state parameters passed as arguments (not from state directly), the setTimeout is unnecessary.

Apply this diff to simplify:

   setStatusFilter([]);
   setExecutionCountMin(null);
   setExecutionCountMax(null);
   setFilePathFilter("");
-  // Reset will trigger effect to fetch unfiltered data
-  setTimeout(() => {
-    fetchFileHistories(1, pagination.pageSize);
-  }, 0);
+  fetchFileHistories(1, pagination.pageSize);
 };
backend/workflow_manager/workflow_v2/file_history_views.py (1)

28-47: Consider adding exception chaining for better error context.

Line 47 raises ValidationError without chaining the original exception. Adding from err would preserve the full error context for debugging.

Apply this diff:

         except (ValueError, TypeError):
-            raise ValidationError({param_name: "Must be a valid integer"})
+            raise ValidationError({param_name: "Must be a valid integer"}) from None

Note: Using from None suppresses the traceback since the ValueError/TypeError is expected and the ValidationError message is sufficient.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 127f129 and 2b27112.

📒 Files selected for processing (11)
  • backend/workflow_manager/workflow_v2/file_history_helper.py (5 hunks)
  • backend/workflow_manager/workflow_v2/file_history_views.py (1 hunks)
  • backend/workflow_manager/workflow_v2/migrations/0018_filehistory_execution_count_and_more.py (1 hunks)
  • backend/workflow_manager/workflow_v2/models/file_history.py (4 hunks)
  • backend/workflow_manager/workflow_v2/models/workflow.py (3 hunks)
  • backend/workflow_manager/workflow_v2/views.py (1 hunks)
  • frontend/src/components/agency/agency/Agency.jsx (5 hunks)
  • frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (1 hunks)
  • workers/api-deployment/tasks.py (3 hunks)
  • workers/shared/processing/files/processor.py (1 hunks)
  • workers/shared/processing/filter_pipeline.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/workflow_manager/workflow_v2/views.py
  • backend/workflow_manager/workflow_v2/models/workflow.py
🧰 Additional context used
🧬 Code graph analysis (7)
frontend/src/components/agency/agency/Agency.jsx (2)
frontend/src/components/pipelines-or-deployments/pipelines/Pipelines.jsx (1)
  • isClearingFileHistory (70-71)
frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (1)
  • FileHistoryModal (33-621)
backend/workflow_manager/workflow_v2/models/file_history.py (2)
backend/workflow_manager/workflow_v2/models/workflow.py (3)
  • Workflow (34-133)
  • WorkflowType (35-40)
  • get_max_execution_count (109-122)
backend/workflow_manager/workflow_v2/serializers.py (1)
  • get_max_execution_count (141-150)
workers/shared/processing/filter_pipeline.py (2)
backend/workflow_manager/workflow_v2/models/file_history.py (1)
  • has_exceeded_limit (28-45)
unstract/core/src/unstract/core/data_models.py (1)
  • ExecutionStatus (336-369)
workers/shared/processing/files/processor.py (2)
backend/workflow_manager/workflow_v2/views.py (1)
  • status (507-548)
unstract/core/src/unstract/core/data_models.py (1)
  • ExecutionStatus (336-369)
workers/api-deployment/tasks.py (1)
unstract/core/src/unstract/core/data_models.py (1)
  • ExecutionStatus (336-369)
backend/workflow_manager/workflow_v2/file_history_helper.py (2)
backend/workflow_manager/workflow_v2/models/file_history.py (2)
  • FileHistory (15-146)
  • update (128-146)
unstract/core/src/unstract/core/data_models.py (1)
  • ExecutionStatus (336-369)
backend/workflow_manager/workflow_v2/file_history_views.py (5)
backend/workflow_manager/workflow_v2/models/file_history.py (1)
  • FileHistory (15-146)
backend/workflow_manager/workflow_v2/permissions.py (1)
  • IsWorkflowOwnerOrShared (7-45)
backend/workflow_manager/workflow_v2/serializers.py (1)
  • FileHistorySerializer (133-161)
workers/shared/processing/filter_pipeline.py (1)
  • _create_cache_key (107-120)
backend/utils/cache_service.py (1)
  • clear_cache_optimized (46-129)
🪛 Ruff (0.14.5)
backend/workflow_manager/workflow_v2/migrations/0018_filehistory_execution_count_and_more.py

10-12: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


14-59: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

backend/workflow_manager/workflow_v2/file_history_helper.py

368-368: Consider moving this statement to an else block

(TRY300)

backend/workflow_manager/workflow_v2/file_history_views.py

25-25: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


45-45: Consider moving this statement to an else block

(TRY300)


47-47: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


84-84: Unused method argument: request

(ARG002)


84-84: Unused method argument: id

(ARG002)


156-156: Do not catch blind exception: Exception

(BLE001)


168-168: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (7)
workers/api-deployment/tasks.py (2)

28-28: Importing ApiDeploymentResultStatus is appropriate for API-specific status mapping

The new import is correctly scoped and used only where API deployment results are constructed, keeping worker-level statuses aligned with core models.


527-548: Cached API results now surface correct status and error information

Mapping cached ExecutionStatus to ApiDeploymentResultStatus and propagating error plus cached_status in metadata fixes the previous “always-success/no-error” behavior for cached responses and makes the API output consistent with underlying execution state. This block looks correct and matches the caching contract introduced in _check_file_history_api.

backend/workflow_manager/workflow_v2/migrations/0018_filehistory_execution_count_and_more.py (1)

1-59: Migration schema matches the model changes and indexing goals

The migration cleanly introduces execution_count, max_file_execution_count, and the expected workflow/file_history indexes (including the trigram GIN index). The operations align with the model snippet and should support the new query/limit semantics without issues.

RUF012 warnings about dependencies/operations being mutable class attributes can be safely ignored here; this is standard Django migration structure.

backend/workflow_manager/workflow_v2/models/file_history.py (3)

24-26: LGTM!

The string representation is helpful for debugging and includes the relevant fields.


66-69: No issues found.

The execution_count field is incremented using F("execution_count") + 1 after processing, which aligns perfectly with the default=1. A newly created FileHistory record starts at 1 (first processing), and subsequent increments reflect additional processing attempts. The semantics are consistent.


3-3: The migration already properly handles the PostgreSQL extension.

Migration 0018 correctly includes TrigramExtension() as the first operation (before the index creation), and the GinIndex properly specifies opclasses=["gin_trgm_ops"]. No changes needed.

backend/workflow_manager/workflow_v2/file_history_views.py (1)

159-169: LGTM!

The broad exception handling (Line 168) is appropriate for cache operations where failures should not block the main delete operation. The warning log ensures issues are tracked without interrupting the workflow.

muhammad-ali-e and others added 4 commits November 24, 2025 20:33
…th_file_history_viewer' of github.com:Zipstack/unstract into feat/UN-3011-FEAT_limit_maximum_file_execution_count_with_file_history_viewer
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
backend/workflow_manager/workflow_v2/file_history_views.py (1)

29-48: Consider exception chaining for improved debugging.

The validation logic is correct. For slightly better debugging context, consider chaining the exception to preserve the original error traceback.

Apply this diff:

         except (ValueError, TypeError):
-            raise ValidationError({param_name: "Must be a valid integer"})
+            raise ValidationError({param_name: "Must be a valid integer"}) from None

Adding from None suppresses the underlying exception (which is intentional here since we're providing a clearer message), or use from err if you want to preserve the chain.

frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (1)

157-162: Consider useCallback for fetchFileHistories to satisfy exhaustive-deps.

The useEffect dependency array omits fetchFileHistories and pagination.pageSize. While functionally correct (you only want to refetch when the modal opens), this may trigger linter warnings.

Optionally wrap fetchFileHistories with useCallback:

const fetchFileHistories = useCallback(async (page = 1, pageSize = 10) => {
  // ... existing implementation
}, [workflowId, statusFilter, executionCountMin, executionCountMax, filePathFilter]);

Then update the useEffect:

useEffect(() => {
  if (open && workflowId) {
    fetchFileHistories(1, pagination.pageSize);
    setSelectedRowKeys([]);
  }
}, [open, workflowId, fetchFileHistories, pagination.pageSize]);

This eliminates the linter warning while preserving the current behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between a6886f5 and 6c17c00.

📒 Files selected for processing (4)
  • backend/workflow_manager/workflow_v2/file_history_helper.py (5 hunks)
  • backend/workflow_manager/workflow_v2/file_history_views.py (1 hunks)
  • frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (1 hunks)
  • frontend/src/components/workflows/workflow/workflow-service.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
backend/workflow_manager/workflow_v2/file_history_helper.py (2)
backend/workflow_manager/workflow_v2/models/file_history.py (1)
  • FileHistory (15-146)
unstract/core/src/unstract/core/data_models.py (1)
  • ExecutionStatus (336-369)
frontend/src/components/workflows/workflow/workflow-service.js (5)
frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx (2)
  • options (27-27)
  • axiosPrivate (53-53)
frontend/src/components/agency/agency/Agency.jsx (1)
  • axiosPrivate (69-69)
frontend/src/components/pipelines-or-deployments/pipelines/Pipelines.jsx (1)
  • axiosPrivate (68-68)
frontend/src/components/deployments/api-deployment/ApiDeployment.jsx (1)
  • axiosPrivate (62-62)
frontend/src/components/pipelines-or-deployments/etl-task-deploy/EtlTaskDeploy.jsx (1)
  • axiosPrivate (45-45)
backend/workflow_manager/workflow_v2/file_history_views.py (3)
backend/workflow_manager/workflow_v2/models/file_history.py (1)
  • FileHistory (15-146)
backend/workflow_manager/workflow_v2/permissions.py (1)
  • IsWorkflowOwnerOrShared (7-45)
backend/workflow_manager/workflow_v2/serializers.py (1)
  • FileHistorySerializer (133-161)
🪛 Ruff (0.14.6)
backend/workflow_manager/workflow_v2/file_history_helper.py

368-368: Consider moving this statement to an else block

(TRY300)

backend/workflow_manager/workflow_v2/file_history_views.py

26-26: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


46-46: Consider moving this statement to an else block

(TRY300)


48-48: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


85-85: Unused method argument: request

(ARG002)


85-85: Unused method argument: id

(ARG002)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (10)
backend/workflow_manager/workflow_v2/file_history_views.py (4)

1-18: LGTM!

Imports are appropriate and the MAX_BULK_DELETE_LIMIT constant is well-defined for protecting against excessive bulk deletions.


21-27: LGTM!

ViewSet configuration is correct. The permission classes ensure proper access control, and the use of CustomPagination provides consistent pagination behavior.

Note: The static analysis hint about ClassVar annotation on line 26 is a false positive; Django REST Framework class attributes should not use ClassVar.


50-83: LGTM!

The queryset filtering is well-implemented with proper validation. The workflow caching optimization avoids redundant database queries, and the filters support flexible querying with appropriate validation.


85-96: LGTM!

Single deletion is correctly implemented with appropriate logging. The static analysis warnings about unused parameters are false positives—these parameters are part of the DRF method signature.

frontend/src/components/workflows/workflow/workflow-service.js (1)

71-112: LGTM! Clean API integration.

The four new methods follow the established patterns in this service. CSRF token handling is appropriate (GET omits it, DELETE and POST include it), and the URL construction is consistent with existing endpoints.

backend/workflow_manager/workflow_v2/file_history_helper.py (2)

241-294: LGTM! Atomic update implementation is solid.

The helpers are well-designed:

  • _safe_str correctly handles None explicitly (avoiding the falsy-value issue)
  • _truncate_hash improves log readability
  • _increment_file_history uses F("execution_count") + 1 for atomic increment and properly refreshes from the database to retrieve updated values

296-403: LGTM! Race condition handling is thorough.

The three-tier approach correctly handles concurrent file history creation:

  1. Check for existing record and increment if found
  2. Create new record if none exists
  3. Handle IntegrityError by fetching and incrementing the record created by another worker

The atomic F() expression in _increment_file_history ensures the execution count is safely incremented even under concurrent access.

Note: The static analysis hint (TRY300) about moving line 368 to an else block is a false positive—the current structure is idiomatic and correct.

frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (3)

204-237: LGTM! Bulk delete implementation is optimal.

The current implementation uses a dedicated backend endpoint (bulkDeleteFileHistoriesByIds) rather than multiple individual delete calls. This is superior to the previous approach suggested in past reviews—the backend can handle partial failures, transaction management, and atomicity better than client-side orchestration with Promise.allSettled.

The 100-item limit (line 207) prevents overwhelming the backend.


240-316: LGTM! Smart two-phase bulk clear workflow.

The implementation correctly:

  • Fetches count with page_size: 1 to minimize data transfer (line 245)
  • Shows confirmation dialog with the exact count before deletion
  • Constructs filter parameters consistently with fetchFileHistories
  • Resets to page 1 after clearing to avoid displaying empty pages

327-630: LGTM! Well-designed responsive UI with good UX.

Highlights:

  • Copy-to-clipboard buttons for file paths and error messages improve usability
  • Execution count display gracefully handles unlimited scenarios ( symbol)
  • Responsive column visibility (responsive: ["md"]) optimizes mobile experience
  • Proper use of tooltips and ellipsis for long text
  • Table scrolling and pagination configured appropriately

@github-actions
Copy link
Contributor

github-actions bot commented Dec 1, 2025

Test Results

Summary
  • Runner Tests: 11 passed, 0 failed (11 total)
  • SDK1 Tests: 66 passed, 0 failed (66 total)

Runner Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_logs}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_cleanup\_skip}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_client\_init}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_container\_run\_config\_without\_mount}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_run\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_get\_image\_for\_sidecar}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{runner/src/unstract/runner/clients/test\_docker.py}}$$ $$\textcolor{#23d18b}{\tt{test\_sidecar\_container}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{11}}$$ $$\textcolor{#23d18b}{\tt{11}}$$
SDK1 Tests - Full Report
filepath function $$\textcolor{#23d18b}{\tt{passed}}$$ SUBTOTAL
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_non\_retryable\_http\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retryable\_http\_errors}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_post\_method\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_platform.py}}$$ $$\textcolor{#23d18b}{\tt{TestPlatformHelperRetry.test\_retry\_logging}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_success\_on\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_retry\_on\_errors}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/test\_prompt.py}}$$ $$\textcolor{#23d18b}{\tt{TestPromptToolRetry.test\_wrapper\_methods\_retry}}$$ $$\textcolor{#23d18b}{\tt{4}}$$ $$\textcolor{#23d18b}{\tt{4}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_connection\_error\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_timeout\_is\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{3}}$$ $$\textcolor{#23d18b}{\tt{3}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_non\_retryable\_status\_codes}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_http\_error\_without\_response}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{5}}$$ $$\textcolor{#23d18b}{\tt{5}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_os\_error\_non\_retryable\_errno}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestIsRetryableError.test\_other\_exception\_not\_retryable}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_without\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_exponential\_backoff\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCalculateDelay.test\_max\_delay\_cap\_with\_jitter}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_successful\_call\_first\_attempt}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_after\_transient\_failure}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_retries\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_max\_time\_exceeded}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_retry\_with\_custom\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_no\_retry\_with\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_exception\_not\_in\_tuple\_not\_retried}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryWithExponentialBackoff.test\_delay\_would\_exceed\_max\_time}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_default\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_environment\_variable\_configuration}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_retries}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_max\_time}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_base\_delay}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_invalid\_multiplier}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_jitter\_values}}$$ $$\textcolor{#23d18b}{\tt{2}}$$ $$\textcolor{#23d18b}{\tt{2}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_exceptions\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_custom\_predicate\_only}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_both\_exceptions\_and\_predicate}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestCreateRetryDecorator.test\_exceptions\_match\_but\_predicate\_false}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_platform\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_retry\_prompt\_service\_call\_exists}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_platform\_service\_decorator\_retries\_on\_connection\_error}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestPreconfiguredDecorators.test\_prompt\_service\_decorator\_retries\_on\_timeout}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_warning\_logged\_on\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_info\_logged\_on\_success\_after\_retry}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{tests/utils/test\_retry\_utils.py}}$$ $$\textcolor{#23d18b}{\tt{TestRetryLogging.test\_exception\_logged\_on\_giving\_up}}$$ $$\textcolor{#23d18b}{\tt{1}}$$ $$\textcolor{#23d18b}{\tt{1}}$$
$$\textcolor{#23d18b}{\tt{TOTAL}}$$ $$\textcolor{#23d18b}{\tt{66}}$$ $$\textcolor{#23d18b}{\tt{66}}$$

@sonarqubecloud
Copy link

sonarqubecloud bot commented Dec 1, 2025

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (7)
backend/workflow_manager/workflow_v2/file_history_views.py (3)

29-48: Validation helper is solid; optionally preserve original exception context

_validate_execution_count correctly centralizes integer/non‑negative validation and raises ValidationError with clear messages. To satisfy tools like Ruff (B904) and aid debugging, you could optionally chain the original exception:

-        except (ValueError, TypeError):
-            raise ValidationError({param_name: "Must be a valid integer"})
+        except (ValueError, TypeError) as exc:
+            raise ValidationError({param_name: "Must be a valid integer"}) from exc

This is purely a nicety; current behavior is functionally fine.


85-96: Clean up unused request / id parameters in destroy

destroy doesn’t use its request or id arguments, which triggers linters (ARG002). If you want to quiet those without changing behavior, you can rename them to underscore‑prefixed:

-    def destroy(self, request, workflow_id=None, id=None):
+    def destroy(self, _request, workflow_id=None, _id=None):

DRF will still route DELETE calls correctly.


98-178: Clear endpoint safeguards and filters are well-structured; consider reusing cached workflow

The clear action now:

  • Requires at least one of ids, status, execution_count_min/max, or file_path before proceeding.
  • Enforces MAX_BULK_DELETE_LIMIT for ID‑based deletes.
  • Reuses _validate_execution_count for body filters.
  • Restricts deletes to the specified workflow.

This addresses the earlier “delete everything with empty body” risk nicely. One small optimization would be to reuse the workflow already cached on request (as in get_queryset) to avoid an extra DB lookup:

-        workflow = get_object_or_404(Workflow, id=workflow_id)
+        if hasattr(request, "_workflow_cache"):
+            workflow = request._workflow_cache
+        else:
+            workflow = get_object_or_404(Workflow, id=workflow_id)

Not required for correctness, just a minor efficiency/consistency tweak.

frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (4)

111-129: Consider removing or gating console.* logging in fetchFileHistoriesWithFilters

The debug console.log("Fetching file histories", ...) and detailed console.error logging are useful during development but can be noisy in production consoles. Since you already surface a user‑facing alert, you could:

  • Remove the console.* calls, or
  • Wrap them in a development check (e.g., if (process.env.NODE_ENV !== "production")).

Functionality is fine either way; this is just cleanliness/observability hygiene.

Also applies to: 162-192


224-240: Resetting filters should probably also clear any existing row selection

handleResetFilters clears all filter state and reapplies an empty filter set but leaves selectedRowKeys untouched. That means after a reset, the “Delete Selected (N)” button can still reflect selections from the previous filter context, which may be surprising.

You could align reset behavior with apply/refresh by also clearing selection:

  const handleResetFilters = () => {
    setStatusFilter([]);
    setExecutionCountMin(null);
    setExecutionCountMax(null);
    setFilePathFilter("");
+   setSelectedRowKeys([]);
    // Reset applied filters

Pure UX consistency; current behavior isn’t broken.


247-297: Guard destructive actions when workflowId is missing, or make workflowId required

Handlers like handleDeleteSingle, handleDeleteSelected, handlePrepareBulkClear, and performBulkClear assume workflowId is defined and pass it straight into the API service. If this modal is ever rendered without a valid workflowId (it’s currently optional in propTypes), these calls will fail in less obvious ways.

Two options:

  • Mark workflowId as required in propTypes, or
  • Add a simple guard at the top of each handler, similar to fetchFileHistoriesWithFilters:
  const handleDeleteSelected = async () => {
-   if (selectedRowKeys.length === 0) return;
+   if (!workflowId || selectedRowKeys.length === 0) return;
    …
  };

Same pattern can be applied to the other handlers. This makes the component more robust against misuse.

Also applies to: 300-390


263-287: Bulk delete / clear flows align well with backend constraints

The bulk delete by IDs honors the frontend MAX_BULK_DELETE of 100 and delegates to a single bulk API call, and the “Clear with Filters” workflow fetches a count first and only enables the button when filters are applied, matching the backend clear safeguard.

To keep things future‑proof, you might add a comment noting that MAX_BULK_DELETE should stay in sync with the backend’s MAX_BULK_DELETE_LIMIT, but otherwise the flows look solid.

Also applies to: 635-685

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 6c17c00 and 1716258.

📒 Files selected for processing (2)
  • backend/workflow_manager/workflow_v2/file_history_views.py (1 hunks)
  • frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (1 hunks)
🧰 Additional context used
🪛 Ruff (0.14.6)
backend/workflow_manager/workflow_v2/file_history_views.py

26-26: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


46-46: Consider moving this statement to an else block

(TRY300)


48-48: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


85-85: Unused method argument: request

(ARG002)


85-85: Unused method argument: id

(ARG002)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
backend/workflow_manager/workflow_v2/file_history_views.py (1)

50-83: Queryset construction and filtering look correct and consistent with API contract

The use of the cached workflow, status IN filter, execution_count_min/max via _validate_execution_count, and file_path__icontains all look correct and align with the expected query parameters from the frontend. Ordering by -created_at is also a sensible default for a history view. No changes needed here.

frontend/src/components/pipelines-or-deployments/file-history-modal/FileHistoryModal.jsx (1)

448-458: This concern is invalid due to backend validation constraints.

The max_file_execution_count field in the Workflow model has MinValueValidator(1) (backend/workflow_manager/workflow_v2/models/workflow.py, line 75), which prevents it from ever being set to 0. The get_max_execution_count() method always returns an integer >= 1 or falls back to settings.MAX_FILE_EXECUTION_COUNT (default: 3). Consequently, the value will never be falsy at the frontend, making the distinction between || and ?? moot. The current implementation is correct.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@chandrasekharan-zipstack chandrasekharan-zipstack left a comment

Choose a reason for hiding this comment

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

LGTM but try to address the NIT comments

return None

@staticmethod
def _safe_str(value: Any) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: Can be a utility method

try:
int_value = int(value)
if int_value < 0:
raise ValidationError({param_name: "Must be a non-negative integer"})
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: Hope such errors get rendered / parsed correctly by the FE code. It seems different from other error messages

# Check status - only return COMPLETED results
status = file_history_data.get("status")

from unstract.core.data_models import ExecutionStatus
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: Move this to the top

@chandrasekharan-zipstack
Copy link
Contributor

@muhammad-ali-e make sure to raise a docs PR as well for this

@muhammad-ali-e
Copy link
Contributor Author

@muhammad-ali-e make sure to raise a docs PR as well for this

once it went to staging

os.environ.get("MAX_PARALLEL_FILE_BATCHES_MAX_VALUE", 100)
)
# Maximum number of times a file can be executed in a workflow
MAX_FILE_EXECUTION_COUNT = int(os.environ.get("MAX_FILE_EXECUTION_COUNT", 3))
Copy link
Contributor

Choose a reason for hiding this comment

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

@muhammad-ali-e is this applicable for Any workflows or ETL alone?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Any Workflow except API. in another word only for ETL and Task workflows

Comment on lines -425 to -433
except Exception as e:
logger.warning(f"FileHistoryFilter - Error checking status {status}: {e}")
# Fallback to original is_completed logic if status checking fails
if not is_completed:
logger.info(
f"FileHistoryFilter - {file_hash.file_name}: ACCEPT "
f"(fallback: history exists but not completed, status={status})"
)
return False
Copy link
Contributor

Choose a reason for hiding this comment

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

@muhammad-ali-e why was this try except removed. Is it safe?

Copy link
Contributor

@ritwik-g ritwik-g left a comment

Choose a reason for hiding this comment

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

@muhammad-ali-e please get the UI code reviewed by FE folks.

Copy link
Contributor

@jagadeeswaran-zipstack jagadeeswaran-zipstack left a comment

Choose a reason for hiding this comment

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

Please move all inline css to external css file. Remove all the logs in FE and use optional chaining where ever required.

Comment on lines +99 to +107
const copyToClipboard = async (text, label = "Text") => {
try {
await navigator.clipboard.writeText(text);
message.success(`${label} copied to clipboard!`);
} catch (err) {
console.error("Failed to copy:", err);
message.error("Failed to copy to clipboard");
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

CopyToClipboard util func should be available in getStaticData,js file please make use o that.

filters = null
) => {
if (!workflowId) {
console.error("FileHistoryModal: workflowId is missing");
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use UseAlert() hook for alert messages. Don't use console.

params
);

console.log("File histories response:", response);
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove all the log in FE. Please log in backend if needed.

Comment on lines +172 to +178
console.error("Error fetching file histories:", err);
console.error("Error details:", {
message: err?.message,
response: err?.response,
status: err?.response?.status,
data: err?.response?.data,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment on lines +313 to +314
appliedFilters.executionCountMin !== null &&
appliedFilters.executionCountMin !== undefined
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use optional chaining where ever required.

Comment on lines +409 to +410
<Space size="small" style={{ width: "100%" }}>
<Text ellipsis={{ tooltip: text }} style={{ flex: 1, maxWidth: 400 }}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Move this styling into external css files. Inline css get hard to manage.

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.

5 participants