diff --git a/api/db/campaign_client.py b/api/db/campaign_client.py index d58d0a7..2e0a53b 100644 --- a/api/db/campaign_client.py +++ b/api/db/campaign_client.py @@ -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: diff --git a/api/db/filters.py b/api/db/filters.py index 0630c66..f515f33 100644 --- a/api/db/filters.py +++ b/api/db/filters.py @@ -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("->>")( diff --git a/api/db/workflow_run_client.py b/api/db/workflow_run_client.py index 25a5f84..657804e 100644 --- a/api/db/workflow_run_client.py +++ b/api/db/workflow_run_client.py @@ -2,12 +2,12 @@ import uuid from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple -from sqlalchemy import Float, func +from sqlalchemy import func 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, @@ -134,21 +134,8 @@ class WorkflowRunClient(BaseDBClient): count_result = await session.execute(count_query) total_count = count_result.scalar() - # Determine sort column - if sort_by == "duration": - # Sort by call_duration_seconds from usage_info JSON field - sort_column = WorkflowRunModel.usage_info.op("->>")("call_duration_seconds").cast(Float) - else: - # Default to created_at - sort_column = WorkflowRunModel.created_at - - # Apply sort order - if sort_order == "asc": - order_clause = sort_column.asc().nullslast() - else: - order_clause = sort_column.desc().nullslast() - - # 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( @@ -245,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 @@ -271,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( diff --git a/api/routes/campaign.py b/api/routes/campaign.py index cd61169..1dc8b9d 100644 --- a/api/routes/campaign.py +++ b/api/routes/campaign.py @@ -1,7 +1,8 @@ +import json from datetime import datetime from typing import List, Optional -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from api.constants import DEFAULT_CAMPAIGN_RETRY_CONFIG, DEFAULT_ORG_CONCURRENCY_LIMIT @@ -91,6 +92,16 @@ class WorkflowRunResponse(BaseModel): completed_at: Optional[datetime] +class CampaignRunsResponse(BaseModel): + """Paginated response for campaign workflow runs""" + + runs: List[dict] # WorkflowRunResponseSchema from schemas + total_count: int + page: int + limit: int + total_pages: int + + class CampaignProgressResponse(BaseModel): campaign_id: int state: str @@ -296,21 +307,65 @@ async def pause_campaign( @router.get("/{campaign_id}/runs") async def get_campaign_runs( campaign_id: int, + page: int = 1, + limit: int = 50, + filters: Optional[str] = Query(None, description="JSON-encoded filter criteria"), + sort_by: Optional[str] = Query( + None, description="Field to sort by (e.g., 'duration', 'created_at')" + ), + sort_order: Optional[str] = Query( + "desc", description="Sort order ('asc' or 'desc')" + ), user: UserModel = Depends(get_user), -) -> List[WorkflowRunResponse]: - """Get campaign workflow runs""" - runs = await db_client.get_campaign_runs(campaign_id, user.selected_organization_id) +) -> CampaignRunsResponse: + """Get campaign workflow runs with pagination, filters and sorting""" + offset = (page - 1) * limit - return [ - WorkflowRunResponse( - id=run.id, - workflow_id=run.workflow_id, - state="completed" if run.is_completed else "running", - created_at=run.created_at, - completed_at=run.created_at if run.is_completed else None, + # Parse filters if provided + filter_criteria = [] + if filters: + try: + filter_criteria = json.loads(filters) + except json.JSONDecodeError: + raise HTTPException(status_code=400, detail="Invalid filter format") + + # Restrict allowed filter attributes for regular users + allowed_attributes = { + "dateRange", + "dispositionCode", + "duration", + "status", + "tokenUsage", + } + for filter_item in filter_criteria: + attribute = filter_item.get("attribute") + if attribute and attribute not in allowed_attributes: + raise HTTPException( + status_code=403, detail=f"Invalid attribute '{attribute}'" + ) + + try: + runs, total_count = await db_client.get_campaign_runs_paginated( + campaign_id, + user.selected_organization_id, + limit=limit, + offset=offset, + filters=filter_criteria if filter_criteria else None, + sort_by=sort_by, + sort_order=sort_order, ) - for run in runs - ] + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + total_pages = (total_count + limit - 1) // limit + + return CampaignRunsResponse( + runs=[run.model_dump() for run in runs], + total_count=total_count, + page=page, + limit=limit, + total_pages=total_pages, + ) @router.post("/{campaign_id}/resume") diff --git a/api/routes/superuser.py b/api/routes/superuser.py index 00ba8fb..e6c10aa 100644 --- a/api/routes/superuser.py +++ b/api/routes/superuser.py @@ -105,8 +105,12 @@ async def get_workflow_runs( page: int = Query(1, ge=1, description="Page number (starts from 1)"), limit: int = Query(50, ge=1, le=100, description="Number of items per page"), filters: Optional[str] = Query(None, description="JSON-encoded filter criteria"), - sort_by: Optional[str] = Query(None, description="Field to sort by (e.g., 'duration', 'created_at')"), - sort_order: Optional[str] = Query("desc", description="Sort order ('asc' or 'desc')"), + sort_by: Optional[str] = Query( + None, description="Field to sort by (e.g., 'duration', 'created_at')" + ), + sort_order: Optional[str] = Query( + "desc", description="Sort order ('asc' or 'desc')" + ), user: UserModel = Depends(get_superuser), ) -> SuperuserWorkflowRunsListResponse: """ @@ -131,7 +135,11 @@ async def get_workflow_runs( sort_order = "desc" workflow_runs, total_count = await db_client.get_workflow_runs_for_superadmin( - limit=limit, offset=offset, filters=filter_criteria, sort_by=sort_by, sort_order=sort_order + limit=limit, + offset=offset, + filters=filter_criteria, + sort_by=sort_by, + sort_order=sort_order, ) total_pages = (total_count + limit - 1) // limit # Ceiling division diff --git a/api/routes/workflow.py b/api/routes/workflow.py index 126314e..9967005 100644 --- a/api/routes/workflow.py +++ b/api/routes/workflow.py @@ -666,10 +666,16 @@ async def get_workflow_runs( page: int = 1, limit: int = 50, filters: Optional[str] = Query(None, description="JSON-encoded filter criteria"), + sort_by: Optional[str] = Query( + None, description="Field to sort by (e.g., 'duration', 'created_at')" + ), + sort_order: Optional[str] = Query( + "desc", description="Sort order ('asc' or 'desc')" + ), user: UserModel = Depends(get_user), ) -> WorkflowRunsResponse: """ - Get workflow runs with optional filtering. + Get workflow runs with optional filtering and sorting. Filters should be provided as a JSON-encoded array of filter criteria. Example: [{"attribute": "dateRange", "value": {"from": "2024-01-01", "to": "2024-01-31"}}] @@ -699,23 +705,15 @@ async def get_workflow_runs( status_code=403, detail=f"Invalid attribute '{attribute}'" ) - # Apply filters if any - if filter_criteria: - runs, total_count = await db_client.get_workflow_runs_by_workflow_id( - workflow_id, - organization_id=user.selected_organization_id, - limit=limit, - offset=offset, - filters=filter_criteria, - ) - else: - # Use existing logic for unfiltered results - runs, total_count = await db_client.get_workflow_runs_by_workflow_id( - workflow_id, - organization_id=user.selected_organization_id, - limit=limit, - offset=offset, - ) + runs, total_count = await db_client.get_workflow_runs_by_workflow_id( + workflow_id, + organization_id=user.selected_organization_id, + limit=limit, + offset=offset, + filters=filter_criteria if filter_criteria else None, + sort_by=sort_by, + sort_order=sort_order, + ) total_pages = (total_count + limit - 1) // limit diff --git a/ui/src/app/campaigns/[campaignId]/page.tsx b/ui/src/app/campaigns/[campaignId]/page.tsx index fc32258..2354b88 100644 --- a/ui/src/app/campaigns/[campaignId]/page.tsx +++ b/ui/src/app/campaigns/[campaignId]/page.tsx @@ -1,36 +1,29 @@ "use client"; import { ArrowLeft, Check, Pause, Play, RefreshCw, X } from 'lucide-react'; -import { useParams, useRouter } from 'next/navigation'; +import { useParams, useRouter, useSearchParams } from 'next/navigation'; import { useCallback, useEffect, useState } from 'react'; import { toast } from 'sonner'; import { getCampaignApiV1CampaignCampaignIdGet, - getCampaignRunsApiV1CampaignCampaignIdRunsGet, getCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGet, pauseCampaignApiV1CampaignCampaignIdPausePost, resumeCampaignApiV1CampaignCampaignIdResumePost, startCampaignApiV1CampaignCampaignIdStartPost} from '@/client/sdk.gen'; -import type { CampaignResponse, WorkflowRunResponse } from '@/client/types.gen'; +import type { CampaignResponse } from '@/client/types.gen'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Separator } from '@/components/ui/separator'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; +import { CampaignRuns } from '@/components/workflow-runs'; import { useAuth } from '@/lib/auth'; export default function CampaignDetailPage() { const { user, getAccessToken, redirectToLogin, loading } = useAuth(); const router = useRouter(); const params = useParams(); + const searchParams = useSearchParams(); const campaignId = parseInt(params.campaignId as string); // Redirect if not authenticated @@ -44,10 +37,6 @@ export default function CampaignDetailPage() { const [campaign, setCampaign] = useState(null); const [isLoadingCampaign, setIsLoadingCampaign] = useState(true); - // Runs state - const [runs, setRuns] = useState([]); - const [isLoadingRuns, setIsLoadingRuns] = useState(false); - // Action state const [isExecutingAction, setIsExecutingAction] = useState(false); @@ -77,36 +66,10 @@ export default function CampaignDetailPage() { } }, [user, getAccessToken, campaignId]); - // Fetch campaign runs - const fetchCampaignRuns = useCallback(async () => { - if (!user) return; - setIsLoadingRuns(true); - try { - const accessToken = await getAccessToken(); - const response = await getCampaignRunsApiV1CampaignCampaignIdRunsGet({ - path: { - campaign_id: campaignId, - }, - headers: { - 'Authorization': `Bearer ${accessToken}`, - } - }); - - if (response.data) { - setRuns(response.data); - } - } catch (error) { - console.error('Failed to fetch campaign runs:', error); - } finally { - setIsLoadingRuns(false); - } - }, [user, getAccessToken, campaignId]); - // Initial load useEffect(() => { fetchCampaign(); - fetchCampaignRuns(); - }, [fetchCampaign, fetchCampaignRuns]); + }, [fetchCampaign]); // Handle back navigation const handleBack = () => { @@ -120,13 +83,6 @@ export default function CampaignDetailPage() { } }; - // Handle run click - const handleRunClick = (runId: number) => { - if (campaign) { - router.push(`/workflow/${campaign.workflow_id}/run/${runId}`); - } - }; - // Handle CSV download const handleDownloadCsv = async () => { if (!user || !campaign || campaign.source_type !== 'csv') return; @@ -497,71 +453,11 @@ export default function CampaignDetailPage() { {/* Workflow Runs */} - - - Workflow Runs - - Executions triggered by this campaign - - - - {isLoadingRuns ? ( -
- {[...Array(3)].map((_, i) => ( -
- ))} -
- ) : runs.length > 0 ? ( -
- - - - Run ID - State - Created - Action - - - - {runs.map((run) => ( - handleRunClick(run.id)} - > - #{run.id} - - - {run.state} - - - {formatDateTime(run.created_at)} - - - - - ))} - -
-
- ) : ( -

- {campaign.state === 'created' - ? 'No runs yet. Start the campaign to begin execution.' - : 'No workflow runs found for this campaign.'} -

- )} -
-
+ ); } diff --git a/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx b/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx index e4f4f09..4e7c51d 100644 --- a/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx +++ b/ui/src/app/workflow/[workflowId]/components/WorkflowExecutions.tsx @@ -1,26 +1,13 @@ "use client"; -import { ChevronLeft, ChevronRight, Download, ExternalLink } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { getWorkflowApiV1WorkflowFetchWorkflowIdGet, getWorkflowRunsApiV1WorkflowWorkflowIdRunsGet } from "@/client/sdk.gen"; import { WorkflowRunResponseSchema } from "@/client/types.gen"; -import { FilterBuilder } from "@/components/filters/FilterBuilder"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; +import { WorkflowRunsTable } from "@/components/workflow-runs"; import { DISPOSITION_CODES } from "@/constants/dispositionCodes"; import { useUserConfig } from '@/context/UserConfigContext'; -import { downloadFile } from "@/lib/files"; import { decodeFiltersFromURL, encodeFiltersToURL } from "@/lib/filters"; import { ActiveFilter, availableAttributes, FilterAttribute } from "@/types/filters"; @@ -43,6 +30,10 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti const [isExecutingFilters, setIsExecutingFilters] = useState(false); const [configuredAttributes, setConfiguredAttributes] = useState(availableAttributes); + // Sort state + const [sortBy, setSortBy] = useState(null); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + const { accessToken } = useUserConfig(); // Initialize filters from URL @@ -50,8 +41,6 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti return decodeFiltersFromURL(searchParams, availableAttributes); }); - const formatDate = (dateString: string) => new Date(dateString).toLocaleString(); - // Load disposition codes from workflow configuration const loadDispositionCodes = useCallback(async () => { if (!accessToken) return; @@ -88,7 +77,12 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti loadDispositionCodes(); }, [loadDispositionCodes]); - const fetchWorkflowRuns = useCallback(async (page: number, filters?: ActiveFilter[]) => { + const fetchWorkflowRuns = useCallback(async ( + page: number, + filters?: ActiveFilter[], + sortByParam?: string | null, + sortOrderParam?: 'asc' | 'desc' + ) => { if (!accessToken) return; try { setLoading(true); @@ -108,7 +102,9 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti query: { page: page, limit: 50, - ...(filterParam && { filters: filterParam }) + ...(filterParam && { filters: filterParam }), + ...(sortByParam && { sort_by: sortByParam }), + ...(sortOrderParam && { sort_order: sortOrderParam }), }, headers: { 'Authorization': `Bearer ${accessToken}`, @@ -151,16 +147,16 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti }, [router, workflowId]); useEffect(() => { - fetchWorkflowRuns(currentPage, activeFilters); - }, [currentPage, activeFilters, fetchWorkflowRuns]); + fetchWorkflowRuns(currentPage, activeFilters, sortBy, sortOrder); + }, [currentPage, activeFilters, fetchWorkflowRuns, sortBy, sortOrder]); const handleApplyFilters = useCallback(async () => { setIsExecutingFilters(true); setCurrentPage(1); // Reset to first page when applying filters updatePageInUrl(1, activeFilters); - await fetchWorkflowRuns(1, activeFilters); + await fetchWorkflowRuns(1, activeFilters, sortBy, sortOrder); setIsExecutingFilters(false); - }, [activeFilters, fetchWorkflowRuns, updatePageInUrl]); + }, [activeFilters, fetchWorkflowRuns, updatePageInUrl, sortBy, sortOrder]); const handleFiltersChange = useCallback((filters: ActiveFilter[]) => { setActiveFilters(filters); @@ -170,182 +166,51 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti setIsExecutingFilters(true); setCurrentPage(1); updatePageInUrl(1, []); // Clear filters from URL - await fetchWorkflowRuns(1, []); // Fetch all workflows without filters + await fetchWorkflowRuns(1, [], sortBy, sortOrder); // Fetch all workflows without filters setIsExecutingFilters(false); - }, [fetchWorkflowRuns, updatePageInUrl]); + }, [fetchWorkflowRuns, updatePageInUrl, sortBy, sortOrder]); + + const handlePageChange = useCallback((page: number) => { + setCurrentPage(page); + updatePageInUrl(page, activeFilters); + }, [updatePageInUrl, activeFilters]); + + const handleSort = useCallback((field: string) => { + // Reset to first page when sort changes + setCurrentPage(1); + + if (sortBy === field) { + // Toggle order if same field + setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc'); + } else { + // New field, default to desc + setSortBy(field); + setSortOrder('desc'); + } + }, [sortBy]); return (
-
-

Workflow Run History

- -
- {loading ? ( -
-
Loading workflow runs...
-
- ) : error ? ( -
- {error} -
- ) : workflowRuns.length === 0 ? ( -
-

No workflow runs found

-
- ) : ( - - - Workflow Runs - - Showing {workflowRuns.length} of {totalCount} total runs - - - -
- - - - ID - Status - Created At - Call Type - Duration - Disposition - Dograh Token - Actions - - - - {workflowRuns.map((run) => ( - window.open(`/workflow/${workflowId}/run/${run.id}`, '_blank')} - > - #{run.id} - - - {run.is_completed ? "Completed" : "In Progress"} - - - {formatDate(run.created_at)} - - - {run.call_type === 'inbound' ? 'Inbound' : 'Outbound'} - - - - {typeof run.cost_info?.call_duration_seconds === 'number' - ? `${run.cost_info.call_duration_seconds.toFixed(1)}s` - : "-"} - - - {run.gathered_context?.mapped_call_disposition ? ( - - {run.gathered_context.mapped_call_disposition as string} - - ) : ( - - - )} - - - {typeof run.cost_info?.dograh_token_usage === 'number' - ? `${run.cost_info.dograh_token_usage.toFixed(2)}` - : "-"} - - -
- {run.transcript_url && ( - - )} - {run.recording_url && ( - - )} - -
-
-
- ))} -
-
-
- - {/* Pagination */} - {totalPages > 1 && ( -
-

- Page {currentPage} of {totalPages} -

-
- - -
-
- )} -
-
- )} +
); } diff --git a/ui/src/client/client.gen.ts b/ui/src/client/client.gen.ts index cbfc632..779eafa 100644 --- a/ui/src/client/client.gen.ts +++ b/ui/src/client/client.gen.ts @@ -1,8 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { ClientOptions } from './types.gen'; -import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch'; +import { type ClientOptions as DefaultClientOptions, type Config, createClient, createConfig } from '@hey-api/client-fetch'; + import { createClientConfig } from '../lib/apiClient'; +import type { ClientOptions } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,4 +17,4 @@ export type CreateClientConfig = export const client = createClient(createClientConfig(createConfig({ baseUrl: 'http://127.0.0.1:8000' -}))); \ No newline at end of file +}))); diff --git a/ui/src/client/index.ts b/ui/src/client/index.ts index e64537d..688e3c9 100644 --- a/ui/src/client/index.ts +++ b/ui/src/client/index.ts @@ -1,3 +1,3 @@ // This file is auto-generated by @hey-api/openapi-ts +export * from './sdk.gen'; export * from './types.gen'; -export * from './sdk.gen'; \ No newline at end of file diff --git a/ui/src/client/sdk.gen.ts b/ui/src/client/sdk.gen.ts index af2f4d9..3995f2c 100644 --- a/ui/src/client/sdk.gen.ts +++ b/ui/src/client/sdk.gen.ts @@ -1,8 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; -import type { InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleCloudonixCdrApiV1TelephonyCloudonixCdrPostData, ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostResponse, ImpersonateApiV1SuperuserImpersonatePostError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetResponse, GetAuthUserApiV1UserAuthUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetResponse, GetApiKeysApiV1UserApiKeysGetError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostResponse, CreateApiKeyApiV1UserApiKeysPostError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostResponse, CreateCampaignApiV1CampaignCreatePostError, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetResponse, GetCampaignsApiV1CampaignGetError, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignApiV1CampaignCampaignIdGetError, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostError, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, PauseCampaignApiV1CampaignCampaignIdPausePostError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, ResumeCampaignApiV1CampaignCampaignIdResumePostError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetResponse, ListCredentialsApiV1CredentialsGetError, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostResponse, CreateCredentialApiV1CredentialsPostError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetError, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutError, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetResponse, ListToolsApiV1ToolsGetError, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostResponse, CreateToolApiV1ToolsPostError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteResponse, DeleteToolApiV1ToolsToolUuidDeleteError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetResponse, GetToolApiV1ToolsToolUuidGetError, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateToolApiV1ToolsToolUuidPutError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetResponse, GetIntegrationsApiV1IntegrationGetError, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostResponse, CreateSessionApiV1IntegrationSessionPostError, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetData, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponse, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetError, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetResponse, GetSignedUrlApiV1S3SignedUrlGetError, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetResponse, GetFileMetadataApiV1S3FileMetadataGetError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetResponse, GetServiceKeysApiV1UserServiceKeysGetError, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateServiceKeyApiV1UserServiceKeysPostError, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetError, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, OptionsInitApiV1PublicEmbedInitOptionsData, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1PublicAgentUuidPostError, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SearchChunksApiV1KnowledgeBaseSearchPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse } from './types.gen'; +import type { Client,Options as ClientOptions, TDataShape } from '@hey-api/client-fetch'; + import { client as _heyApiClient } from './client.gen'; +import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetData, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetError, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixCdrApiV1TelephonyCloudonixCdrPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostError, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; export type Options = ClientOptions & { /** @@ -366,7 +367,7 @@ export const updateWorkflowApiV1WorkflowWorkflowIdPut = (options: Options) => { return (options.client ?? _heyApiClient).get({ @@ -1539,4 +1540,4 @@ export const healthApiV1HealthGet = (optio url: '/api/v1/health', ...options }); -}; \ No newline at end of file +}; diff --git a/ui/src/client/types.gen.ts b/ui/src/client/types.gen.ts index c43475c..d1127ee 100644 --- a/ui/src/client/types.gen.ts +++ b/ui/src/client/types.gen.ts @@ -81,6 +81,19 @@ export type CampaignResponse = { max_concurrency?: number | null; }; +/** + * Paginated response for campaign workflow runs + */ +export type CampaignRunsResponse = { + runs: Array<{ + [key: string]: unknown; + }>; + total_count: number; + page: number; + limit: number; + total_pages: number; +}; + export type CampaignSourceDownloadResponse = { download_url: string; expires_in: number; @@ -1138,14 +1151,6 @@ export type WorkflowRunDetail = { created_at: string; }; -export type WorkflowRunResponse = { - id: number; - workflow_id: number; - state: string; - created_at: string; - completed_at: string | null; -}; - export type WorkflowRunResponseSchema = { id: number; workflow_id: number; @@ -1963,6 +1968,14 @@ export type GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData = { * JSON-encoded filter criteria */ filters?: string | null; + /** + * Field to sort by (e.g., 'duration', 'created_at') + */ + sort_by?: string | null; + /** + * Sort order ('asc' or 'desc') + */ + sort_order?: string | null; }; url: '/api/v1/workflow/{workflow_id}/runs'; }; @@ -2630,7 +2643,22 @@ export type GetCampaignRunsApiV1CampaignCampaignIdRunsGetData = { path: { campaign_id: number; }; - query?: never; + query?: { + page?: number; + limit?: number; + /** + * JSON-encoded filter criteria + */ + filters?: string | null; + /** + * Field to sort by (e.g., 'duration', 'created_at') + */ + sort_by?: string | null; + /** + * Sort order ('asc' or 'desc') + */ + sort_order?: string | null; + }; url: '/api/v1/campaign/{campaign_id}/runs'; }; @@ -2651,7 +2679,7 @@ export type GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponses = { /** * Successful Response */ - 200: Array; + 200: CampaignRunsResponse; }; export type GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse = GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponses[keyof GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponses]; @@ -4691,4 +4719,4 @@ export type HealthApiV1HealthGetResponse = HealthApiV1HealthGetResponses[keyof H export type ClientOptions = { baseUrl: 'http://127.0.0.1:8000' | (string & {}); -}; \ No newline at end of file +}; diff --git a/ui/src/components/workflow-runs/CampaignRuns.tsx b/ui/src/components/workflow-runs/CampaignRuns.tsx new file mode 100644 index 0000000..e94c258 --- /dev/null +++ b/ui/src/components/workflow-runs/CampaignRuns.tsx @@ -0,0 +1,197 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; + +import { getCampaignRunsApiV1CampaignCampaignIdRunsGet } from "@/client/sdk.gen"; +import { WorkflowRunResponseSchema } from "@/client/types.gen"; +import { WorkflowRunsTable } from "@/components/workflow-runs"; +import { useAuth } from "@/lib/auth"; +import { decodeFiltersFromURL, encodeFiltersToURL } from "@/lib/filters"; +import { ActiveFilter, availableAttributes, FilterAttribute } from "@/types/filters"; + +interface CampaignRunsProps { + campaignId: number; + workflowId: number; + searchParams?: URLSearchParams; +} + +export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignRunsProps) { + const router = useRouter(); + const { getAccessToken } = useAuth(); + const [runs, setRuns] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentPage, setCurrentPage] = useState(() => { + const pageParam = searchParams?.get('page'); + return pageParam ? parseInt(pageParam, 10) : 1; + }); + const [totalPages, setTotalPages] = useState(1); + const [totalCount, setTotalCount] = useState(0); + const [isExecutingFilters, setIsExecutingFilters] = useState(false); + const [accessToken, setAccessToken] = useState(null); + + // Sort state + const [sortBy, setSortBy] = useState(null); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + + // Initialize filters from URL + const [activeFilters, setActiveFilters] = useState(() => { + return searchParams ? decodeFiltersFromURL(searchParams, availableAttributes) : []; + }); + + // Get access token on mount + useEffect(() => { + const fetchToken = async () => { + const token = await getAccessToken(); + setAccessToken(token); + }; + fetchToken(); + }, [getAccessToken]); + + const fetchCampaignRuns = useCallback(async ( + page: number, + filters?: ActiveFilter[], + sortByParam?: string | null, + sortOrderParam?: 'asc' | 'desc' + ) => { + if (!accessToken) return; + try { + setLoading(true); + // Prepare filter data for API + let filterParam = undefined; + if (filters && filters.length > 0) { + const filterData = filters.map(filter => ({ + attribute: filter.attribute.id, + type: filter.attribute.type, + value: filter.value + })); + filterParam = JSON.stringify(filterData); + } + + const response = await getCampaignRunsApiV1CampaignCampaignIdRunsGet({ + path: { campaign_id: campaignId }, + query: { + page: page, + limit: 50, + ...(filterParam && { filters: filterParam }), + ...(sortByParam && { sort_by: sortByParam }), + ...(sortOrderParam && { sort_order: sortOrderParam }), + }, + headers: { + 'Authorization': `Bearer ${accessToken}`, + } + }); + + if (response.error) { + throw new Error("Failed to fetch campaign runs"); + } + + if (response.data) { + // The API returns runs as array of dicts, convert to WorkflowRunResponseSchema + setRuns((response.data.runs || []) as unknown as WorkflowRunResponseSchema[]); + setTotalPages(response.data.total_pages || 1); + setTotalCount(response.data.total_count || 0); + setCurrentPage(response.data.page || 1); + } + setError(null); + } catch (err) { + console.error("Error fetching campaign runs:", err); + setError("Failed to load campaign runs"); + } finally { + setLoading(false); + } + }, [campaignId, accessToken]); + + const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[]) => { + const params = new URLSearchParams(); + params.set('page', page.toString()); + + // Add filters to URL if present + if (filters && filters.length > 0) { + const filterString = encodeFiltersToURL(filters); + if (filterString) { + const filterParams = new URLSearchParams(filterString); + filterParams.forEach((value, key) => params.set(key, value)); + } + } + + router.push(`/campaigns/${campaignId}?${params.toString()}`, { scroll: false }); + }, [router, campaignId]); + + useEffect(() => { + if (accessToken) { + fetchCampaignRuns(currentPage, activeFilters, sortBy, sortOrder); + } + }, [currentPage, activeFilters, fetchCampaignRuns, accessToken, sortBy, sortOrder]); + + const handleApplyFilters = useCallback(async () => { + setIsExecutingFilters(true); + setCurrentPage(1); + updatePageInUrl(1, activeFilters); + await fetchCampaignRuns(1, activeFilters, sortBy, sortOrder); + setIsExecutingFilters(false); + }, [activeFilters, fetchCampaignRuns, updatePageInUrl, sortBy, sortOrder]); + + const handleFiltersChange = useCallback((filters: ActiveFilter[]) => { + setActiveFilters(filters); + }, []); + + const handleClearFilters = useCallback(async () => { + setIsExecutingFilters(true); + setCurrentPage(1); + setActiveFilters([]); + updatePageInUrl(1, []); + await fetchCampaignRuns(1, [], sortBy, sortOrder); + setIsExecutingFilters(false); + }, [fetchCampaignRuns, updatePageInUrl, sortBy, sortOrder]); + + const handlePageChange = useCallback((page: number) => { + setCurrentPage(page); + updatePageInUrl(page, activeFilters); + }, [updatePageInUrl, activeFilters]); + + const handleSort = useCallback((field: string) => { + // Reset to first page when sort changes + setCurrentPage(1); + + if (sortBy === field) { + // Toggle order if same field + setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc'); + } else { + // New field, default to desc + setSortBy(field); + setSortOrder('desc'); + } + }, [sortBy]); + + // Use a subset of filter attributes relevant for campaigns + const campaignFilterAttributes: FilterAttribute[] = availableAttributes.filter( + attr => ['dateRange', 'dispositionCode', 'duration', 'status', 'tokenUsage'].includes(attr.id) + ); + + return ( + + ); +} diff --git a/ui/src/components/workflow-runs/WorkflowRunsTable.tsx b/ui/src/components/workflow-runs/WorkflowRunsTable.tsx new file mode 100644 index 0000000..4d161fd --- /dev/null +++ b/ui/src/components/workflow-runs/WorkflowRunsTable.tsx @@ -0,0 +1,251 @@ +"use client"; + +import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, ExternalLink } from "lucide-react"; +import { useState } from "react"; + +import { WorkflowRunResponseSchema } from "@/client/types.gen"; +import { FilterBuilder } from "@/components/filters/FilterBuilder"; +import { MediaPreviewButtons, MediaPreviewDialog } from "@/components/MediaPreviewDialog"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { ActiveFilter, FilterAttribute } from "@/types/filters"; + +export interface WorkflowRunsTableProps { + // Data + runs: WorkflowRunResponseSchema[]; + loading: boolean; + error: string | null; + + // Pagination + currentPage: number; + totalPages: number; + totalCount: number; + onPageChange: (page: number) => void; + + // Filters + availableAttributes: FilterAttribute[]; + activeFilters: ActiveFilter[]; + onFiltersChange: (filters: ActiveFilter[]) => void; + onApplyFilters: () => void; + onClearFilters: () => void; + isExecutingFilters: boolean; + + // Sorting + sortBy?: string | null; + sortOrder?: 'asc' | 'desc'; + onSort?: (field: string) => void; + + // Navigation & Actions + workflowId: number; + accessToken: string | null; + + // Optional customization + title?: string; + subtitle?: string; + showFilters?: boolean; + emptyMessage?: string; +} + +export function WorkflowRunsTable({ + runs, + loading, + error, + currentPage, + totalPages, + totalCount, + onPageChange, + availableAttributes, + activeFilters, + onFiltersChange, + onApplyFilters, + onClearFilters, + isExecutingFilters, + sortBy, + sortOrder = 'desc', + onSort, + workflowId, + accessToken, + title = "Workflow Run History", + subtitle, + showFilters = true, + emptyMessage = "No workflow runs found", +}: WorkflowRunsTableProps) { + const [selectedRowId, setSelectedRowId] = useState(null); + + // Media preview dialog + const mediaPreview = MediaPreviewDialog({ accessToken }); + + const formatDate = (dateString: string) => new Date(dateString).toLocaleString(); + + const handleRowClick = (runId: number) => { + window.open(`/workflow/${workflowId}/run/${runId}`, '_blank'); + }; + + return ( +
+ {/* Title and Filters */} + {showFilters && ( +
+

{title}

+ +
+ )} + + {/* Loading State */} + {loading ? ( +
+
Loading workflow runs...
+
+ ) : error ? ( +
+ {error} +
+ ) : runs.length === 0 ? ( +
+

{emptyMessage}

+
+ ) : ( + + + Workflow Runs + + {subtitle || `Showing ${runs.length} of ${totalCount} total runs`} + + + +
+ + + + ID + Status + Created At + Call Type + onSort?.('duration')} + > +
+ Duration + {sortBy === 'duration' ? ( + sortOrder === 'asc' ? : + ) : ( + + )} +
+
+ Disposition + Actions +
+
+ + {runs.map((run) => ( + handleRowClick(run.id)} + > + #{run.id} + + + {run.is_completed ? "Completed" : "In Progress"} + + + {formatDate(run.created_at)} + + + {run.call_type === 'inbound' ? 'Inbound' : 'Outbound'} + + + + {typeof run.cost_info?.call_duration_seconds === 'number' + ? `${run.cost_info.call_duration_seconds.toFixed(1)}s` + : "-"} + + + {run.gathered_context?.mapped_call_disposition ? ( + + {run.gathered_context.mapped_call_disposition as string} + + ) : ( + - + )} + + +
e.stopPropagation()}> + + +
+
+
+ ))} +
+
+
+ + {/* Pagination */} + {totalPages > 1 && ( +
+

+ Page {currentPage} of {totalPages} +

+
+ + +
+
+ )} +
+
+ )} + + {/* Media Preview Dialog */} + {mediaPreview.dialog} +
+ ); +} diff --git a/ui/src/components/workflow-runs/index.ts b/ui/src/components/workflow-runs/index.ts new file mode 100644 index 0000000..af89077 --- /dev/null +++ b/ui/src/components/workflow-runs/index.ts @@ -0,0 +1,3 @@ +export { CampaignRuns } from "./CampaignRuns"; +export type { WorkflowRunsTableProps } from "./WorkflowRunsTable"; +export { WorkflowRunsTable } from "./WorkflowRunsTable";