mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
chore: UI enhancements for workflow runs view (#142)
* add local state in filters * feat: add sorting feature by duration * chore: refactor workfow run view
This commit is contained in:
parent
6827744327
commit
5fe1c8ce2f
23 changed files with 1014 additions and 479 deletions
|
|
@ -1,11 +1,13 @@
|
|||
from datetime import UTC, datetime
|
||||
from typing import Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from api.db.base_client import BaseDBClient
|
||||
from api.db.filters import apply_workflow_run_filters, get_workflow_run_order_clause
|
||||
from api.db.models import CampaignModel, QueuedRunModel, WorkflowRunModel
|
||||
from api.schemas.workflow import WorkflowRunResponseSchema
|
||||
|
||||
|
||||
class CampaignClient(BaseDBClient):
|
||||
|
|
@ -165,6 +167,89 @@ class CampaignClient(BaseDBClient):
|
|||
result = await session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_campaign_runs_paginated(
|
||||
self,
|
||||
campaign_id: int,
|
||||
organization_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
filters: Optional[List[Dict[str, Any]]] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: Optional[str] = "desc",
|
||||
) -> tuple[list[WorkflowRunResponseSchema], int]:
|
||||
"""Get workflow runs for a campaign with pagination, filters and sorting"""
|
||||
async with self.async_session() as session:
|
||||
# First verify campaign belongs to organization
|
||||
campaign_query = select(CampaignModel).where(
|
||||
CampaignModel.id == campaign_id,
|
||||
CampaignModel.organization_id == organization_id,
|
||||
)
|
||||
campaign_result = await session.execute(campaign_query)
|
||||
campaign = campaign_result.scalar_one_or_none()
|
||||
|
||||
if not campaign:
|
||||
raise ValueError(f"Campaign {campaign_id} not found")
|
||||
|
||||
# Build base query
|
||||
base_query = select(WorkflowRunModel).where(
|
||||
WorkflowRunModel.campaign_id == campaign_id
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
base_query = apply_workflow_run_filters(base_query, filters)
|
||||
|
||||
# Count total with filters
|
||||
count_query = base_query.with_only_columns(func.count(WorkflowRunModel.id))
|
||||
count_result = await session.execute(count_query)
|
||||
total_count = count_result.scalar()
|
||||
|
||||
# Get paginated results with filters and sorting
|
||||
order_clause = get_workflow_run_order_clause(sort_by, sort_order)
|
||||
result = await session.execute(
|
||||
base_query.order_by(order_clause).limit(limit).offset(offset)
|
||||
)
|
||||
|
||||
runs = [
|
||||
WorkflowRunResponseSchema.model_validate(
|
||||
{
|
||||
"id": run.id,
|
||||
"workflow_id": run.workflow_id,
|
||||
"name": run.name,
|
||||
"mode": run.mode,
|
||||
"created_at": run.created_at,
|
||||
"is_completed": run.is_completed,
|
||||
"recording_url": run.recording_url,
|
||||
"transcript_url": run.transcript_url,
|
||||
"cost_info": {
|
||||
"dograh_token_usage": (
|
||||
run.cost_info.get("dograh_token_usage")
|
||||
if run.cost_info
|
||||
and "dograh_token_usage" in run.cost_info
|
||||
else round(
|
||||
float(run.cost_info.get("total_cost_usd", 0)) * 100,
|
||||
2,
|
||||
)
|
||||
if run.cost_info and "total_cost_usd" in run.cost_info
|
||||
else 0
|
||||
),
|
||||
"call_duration_seconds": int(
|
||||
round(run.cost_info.get("call_duration_seconds") or 0)
|
||||
)
|
||||
if run.cost_info
|
||||
else None,
|
||||
}
|
||||
if run.cost_info
|
||||
else None,
|
||||
"definition_id": run.definition_id,
|
||||
"initial_context": run.initial_context,
|
||||
"gathered_context": run.gathered_context,
|
||||
"call_type": run.call_type,
|
||||
}
|
||||
)
|
||||
for run in result.scalars().all()
|
||||
]
|
||||
return runs, total_count
|
||||
|
||||
async def get_campaign_by_id(self, campaign_id: int) -> Optional[CampaignModel]:
|
||||
"""Get campaign by ID without organization check (for internal use)"""
|
||||
async with self.async_session() as session:
|
||||
|
|
|
|||
|
|
@ -3,16 +3,47 @@
|
|||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import Integer, and_, cast, func
|
||||
from sqlalchemy import Float, Integer, and_, cast, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
from api.db.models import WorkflowRunModel
|
||||
|
||||
|
||||
def get_workflow_run_order_clause(
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: str = "desc",
|
||||
):
|
||||
"""
|
||||
Get the order clause for workflow run queries.
|
||||
|
||||
Args:
|
||||
sort_by: Field to sort by ('duration', 'created_at', etc.)
|
||||
sort_order: 'asc' or 'desc'
|
||||
|
||||
Returns:
|
||||
SQLAlchemy order clause
|
||||
"""
|
||||
# Determine sort column
|
||||
if sort_by == "duration":
|
||||
sort_column = WorkflowRunModel.cost_info.op("->>")(
|
||||
"call_duration_seconds"
|
||||
).cast(Float)
|
||||
else:
|
||||
# Default to created_at
|
||||
sort_column = WorkflowRunModel.created_at
|
||||
|
||||
# Apply sort order
|
||||
if sort_order == "asc":
|
||||
return sort_column.asc().nullslast()
|
||||
else:
|
||||
return sort_column.desc().nullslast()
|
||||
|
||||
|
||||
# Mapping of attribute names to database fields
|
||||
ATTRIBUTE_FIELD_MAPPING = {
|
||||
"dateRange": "created_at",
|
||||
"dispositionCode": "gathered_context.mapped_call_disposition",
|
||||
"duration": "usage_info.call_duration_seconds",
|
||||
"duration": "cost_info.call_duration_seconds",
|
||||
"status": "is_completed",
|
||||
"tokenUsage": "cost_info.total_cost_usd",
|
||||
"runId": "id",
|
||||
|
|
@ -153,7 +184,7 @@ def apply_workflow_run_filters(
|
|||
min_val = value.get("min")
|
||||
max_val = value.get("max")
|
||||
|
||||
if field == "usage_info.call_duration_seconds":
|
||||
if field == "cost_info.call_duration_seconds":
|
||||
# Use ->> operator for compatibility with all PostgreSQL versions
|
||||
# (subscript [] only works in PostgreSQL 14+)
|
||||
duration_text = cast(WorkflowRunModel.usage_info, JSONB).op("->>")(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from sqlalchemy.future import select
|
|||
from sqlalchemy.orm import joinedload, selectinload
|
||||
|
||||
from api.db.base_client import BaseDBClient
|
||||
from api.db.filters import apply_workflow_run_filters
|
||||
from api.db.filters import apply_workflow_run_filters, get_workflow_run_order_clause
|
||||
from api.db.models import (
|
||||
OrganizationModel,
|
||||
UserModel,
|
||||
|
|
@ -103,10 +103,16 @@ class WorkflowRunClient(BaseDBClient):
|
|||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
filters: Optional[List[Dict[str, Any]]] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: str = "desc",
|
||||
) -> tuple[list[dict], int]:
|
||||
"""
|
||||
Get paginated workflow runs for superadmin with organization information.
|
||||
Returns tuple of (workflow_runs, total_count).
|
||||
|
||||
Args:
|
||||
sort_by: Field to sort by ('duration', 'created_at', etc.)
|
||||
sort_order: 'asc' or 'desc'
|
||||
"""
|
||||
async with self.async_session() as session:
|
||||
# Build base query with joins
|
||||
|
|
@ -128,7 +134,8 @@ class WorkflowRunClient(BaseDBClient):
|
|||
count_result = await session.execute(count_query)
|
||||
total_count = count_result.scalar()
|
||||
|
||||
# Get paginated results with filters
|
||||
# Get paginated results with filters and sorting
|
||||
order_clause = get_workflow_run_order_clause(sort_by, sort_order)
|
||||
result = await session.execute(
|
||||
base_query.options(
|
||||
joinedload(WorkflowRunModel.workflow).joinedload(
|
||||
|
|
@ -138,7 +145,7 @@ class WorkflowRunClient(BaseDBClient):
|
|||
.joinedload(WorkflowModel.user)
|
||||
.joinedload(UserModel.selected_organization),
|
||||
)
|
||||
.order_by(WorkflowRunModel.created_at.desc())
|
||||
.order_by(order_clause)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
|
|
@ -225,6 +232,8 @@ class WorkflowRunClient(BaseDBClient):
|
|||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
filters: Optional[List[Dict[str, Any]]] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: Optional[str] = "desc",
|
||||
) -> tuple[list[WorkflowRunResponseSchema], int]:
|
||||
async with self.async_session() as session:
|
||||
# Build base query
|
||||
|
|
@ -251,11 +260,10 @@ class WorkflowRunClient(BaseDBClient):
|
|||
count_result = await session.execute(count_query)
|
||||
total_count = count_result.scalar()
|
||||
|
||||
# Get paginated results with filters
|
||||
# Get paginated results with filters and sorting
|
||||
order_clause = get_workflow_run_order_clause(sort_by, sort_order)
|
||||
result = await session.execute(
|
||||
base_query.order_by(WorkflowRunModel.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
base_query.order_by(order_clause).limit(limit).offset(offset)
|
||||
)
|
||||
runs = [
|
||||
WorkflowRunResponseSchema.model_validate(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue