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:
Abhishek 2026-01-30 17:08:15 +05:30 committed by GitHub
parent 6827744327
commit 5fe1c8ce2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1014 additions and 479 deletions

View file

@ -1,27 +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 { getDispositionBadgeVariant } from '@/lib/dispositionBadgeVariant';
import { downloadFile } from "@/lib/files";
import { decodeFiltersFromURL, encodeFiltersToURL } from "@/lib/filters";
import { ActiveFilter, availableAttributes, FilterAttribute } from "@/types/filters";
@ -44,6 +30,10 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti
const [isExecutingFilters, setIsExecutingFilters] = useState(false);
const [configuredAttributes, setConfiguredAttributes] = useState<FilterAttribute[]>(availableAttributes);
// Sort state
const [sortBy, setSortBy] = useState<string | null>(null);
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const { accessToken } = useUserConfig();
// Initialize filters from URL
@ -51,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;
@ -89,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);
@ -109,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}`,
@ -152,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);
@ -171,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 (
<div className="container mx-auto py-8">
<div className="mb-6">
<h1 className="text-2xl font-bold mb-4">Workflow Run History</h1>
<FilterBuilder
availableAttributes={configuredAttributes}
activeFilters={activeFilters}
onFiltersChange={handleFiltersChange}
onApplyFilters={handleApplyFilters}
onClearFilters={handleClearFilters}
isExecuting={isExecutingFilters}
/>
</div>
{loading ? (
<div className="flex justify-center">
<div className="animate-pulse">Loading workflow runs...</div>
</div>
) : error ? (
<div className="bg-destructive/10 border border-destructive/30 text-destructive px-4 py-3 rounded">
{error}
</div>
) : workflowRuns.length === 0 ? (
<div className="text-center py-8">
<p className="text-muted-foreground">No workflow runs found</p>
</div>
) : (
<Card>
<CardHeader>
<CardTitle>Workflow Runs</CardTitle>
<CardDescription>
Showing {workflowRuns.length} of {totalCount} total runs
</CardDescription>
</CardHeader>
<CardContent>
<div className="bg-card border border-border rounded-lg overflow-hidden shadow-sm">
<Table>
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="font-semibold">ID</TableHead>
<TableHead className="font-semibold">Status</TableHead>
<TableHead className="font-semibold">Created At</TableHead>
<TableHead className="font-semibold">Call Type</TableHead>
<TableHead className="font-semibold">Duration</TableHead>
<TableHead className="font-semibold">Disposition</TableHead>
<TableHead className="font-semibold">Dograh Token</TableHead>
<TableHead className="font-semibold">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{workflowRuns.map((run) => (
<TableRow
key={run.id}
className="cursor-pointer"
onClick={() => window.open(`/workflow/${workflowId}/run/${run.id}`, '_blank')}
>
<TableCell className="font-mono text-sm">#{run.id}</TableCell>
<TableCell>
<Badge variant={run.is_completed ? "default" : "secondary"}>
{run.is_completed ? "Completed" : "In Progress"}
</Badge>
</TableCell>
<TableCell className="text-sm">{formatDate(run.created_at)}</TableCell>
<TableCell>
<Badge variant={run.call_type === 'inbound' ? "secondary" : "default"}>
{run.call_type === 'inbound' ? 'Inbound' : 'Outbound'}
</Badge>
</TableCell>
<TableCell className="text-sm">
{typeof run.cost_info?.call_duration_seconds === 'number'
? `${run.cost_info.call_duration_seconds.toFixed(1)}s`
: "-"}
</TableCell>
<TableCell>
{run.gathered_context?.mapped_call_disposition ? (
<Badge variant={getDispositionBadgeVariant(run.gathered_context.mapped_call_disposition as string)}>
{run.gathered_context.mapped_call_disposition as string}
</Badge>
) : (
<span className="text-sm text-muted-foreground">-</span>
)}
</TableCell>
<TableCell className="text-sm">
{typeof run.cost_info?.dograh_token_usage === 'number'
? `${run.cost_info.dograh_token_usage.toFixed(2)}`
: "-"}
</TableCell>
<TableCell>
<div className="flex space-x-2">
{run.transcript_url && (
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
if (accessToken) downloadFile(run.transcript_url, accessToken);
}}
>
<Download className="h-3 w-3 mr-1" />
Transcript
</Button>
)}
{run.recording_url && (
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
if (accessToken) downloadFile(run.recording_url, accessToken);
}}
>
<Download className="h-3 w-3 mr-1" />
Recording
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
window.open(`/workflow/${workflowId}/run/${run.id}`, '_blank');
}}
>
<ExternalLink className="h-3 w-3 mr-1" />
View
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between mt-6">
<p className="text-sm text-muted-foreground">
Page {currentPage} of {totalPages}
</p>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
const newPage = currentPage - 1;
setCurrentPage(newPage);
updatePageInUrl(newPage, activeFilters);
}}
disabled={currentPage === 1}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
const newPage = currentPage + 1;
setCurrentPage(newPage);
updatePageInUrl(newPage, activeFilters);
}}
disabled={currentPage === totalPages}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</CardContent>
</Card>
)}
<WorkflowRunsTable
runs={workflowRuns}
loading={loading}
error={error}
currentPage={currentPage}
totalPages={totalPages}
totalCount={totalCount}
onPageChange={handlePageChange}
availableAttributes={configuredAttributes}
activeFilters={activeFilters}
onFiltersChange={handleFiltersChange}
onApplyFilters={handleApplyFilters}
onClearFilters={handleClearFilters}
isExecutingFilters={isExecutingFilters}
sortBy={sortBy}
sortOrder={sortOrder}
onSort={handleSort}
workflowId={workflowId}
accessToken={accessToken}
/>
</div>
);
}