chore: add sort order in URL params

This commit is contained in:
Abhishek Kumar 2026-02-06 09:01:06 +05:30
parent e8005042e2
commit 9824d88d53
7 changed files with 148 additions and 59 deletions

View file

@ -13,7 +13,7 @@ class RateLimiter:
def __init__(self):
self.redis_client: Optional[aioredis.Redis] = None
self.stale_call_timeout = 1800 # 30 minutes in seconds
self.stale_call_timeout = 300 # 5 minutes in seconds
async def _get_redis(self) -> aioredis.Redis:
"""Get or create Redis connection"""

@ -1 +1 @@
Subproject commit 866bf1c5685e7fadf2af012d8769ebbc35297db0
Subproject commit e618bb98dfde6224ef9f4e15769580790719b269

View file

@ -92,9 +92,14 @@ export default function RunsPage() {
return decodeFiltersFromURL(searchParams, superadminFilterAttributes);
});
// Sort state
const [sortBy, setSortBy] = useState<string | null>(null);
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
// Sort state (initialized from URL)
const [sortBy, setSortBy] = useState<string | null>(() => {
return searchParams.get('sort_by') || null;
});
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(() => {
const order = searchParams.get('sort_order');
return order === 'asc' ? 'asc' : 'desc';
});
// Dialog state for comment editing
const [isCommentDialogOpen, setIsCommentDialogOpen] = useState(false);
@ -167,7 +172,7 @@ export default function RunsPage() {
}
}, [limit, accessToken]);
const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[]) => {
const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[], sortByParam?: string | null, sortOrderParam?: 'asc' | 'desc') => {
const params = new URLSearchParams();
params.set('page', page.toString());
@ -180,6 +185,12 @@ export default function RunsPage() {
}
}
// Add sort to URL if present
if (sortByParam) {
params.set('sort_by', sortByParam);
params.set('sort_order', sortOrderParam || 'desc');
}
router.push(`/superadmin/runs?${params.toString()}`);
}, [router]);
@ -208,7 +219,7 @@ export default function RunsPage() {
const handlePageChange = (page: number) => {
setCurrentPage(page);
updatePageInUrl(page, appliedFilters);
updatePageInUrl(page, appliedFilters, sortBy, sortOrder);
fetchRuns(page, appliedFilters, false, sortBy, sortOrder);
};
@ -216,7 +227,7 @@ export default function RunsPage() {
setIsExecutingFilters(true);
setCurrentPage(1); // Reset to first page when applying filters
setAppliedFilters(activeFilters); // Update applied filters
updatePageInUrl(1, activeFilters);
updatePageInUrl(1, activeFilters, sortBy, sortOrder);
await fetchRuns(1, activeFilters, false, sortBy, sortOrder);
setIsExecutingFilters(false);
}, [activeFilters, fetchRuns, updatePageInUrl, sortBy, sortOrder]);
@ -229,7 +240,7 @@ export default function RunsPage() {
setIsExecutingFilters(true);
setCurrentPage(1);
setAppliedFilters([]); // Clear applied filters
updatePageInUrl(1, []); // Clear filters from URL
updatePageInUrl(1, [], sortBy, sortOrder); // Clear filters from URL
await fetchRuns(1, [], false, sortBy, sortOrder); // Fetch all runs without filters
setIsExecutingFilters(false);
}, [fetchRuns, updatePageInUrl, sortBy, sortOrder]);
@ -238,15 +249,16 @@ export default function RunsPage() {
// Reset to first page when sort changes
setCurrentPage(1);
const newSortBy = field;
let newSortOrder: 'asc' | 'desc' = 'desc';
if (sortBy === field) {
// Toggle order if same field
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
// New field, default to desc
setSortBy(field);
setSortOrder('desc');
newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
}
}, [sortBy]);
setSortBy(newSortBy);
setSortOrder(newSortOrder);
updatePageInUrl(1, appliedFilters, newSortBy, newSortOrder);
}, [sortBy, sortOrder, updatePageInUrl, appliedFilters]);
// Save comment function declared outside JSX (requirement #2)
const saveAdminComment = useCallback(async () => {
@ -345,6 +357,7 @@ export default function RunsPage() {
isExecuting={isExecutingFilters}
autoRefresh={autoRefresh}
onAutoRefreshChange={setAutoRefresh}
hasAppliedFilters={appliedFilters.length > 0}
/>
<Card>

View file

@ -30,9 +30,14 @@ 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');
// Sort state (initialized from URL)
const [sortBy, setSortBy] = useState<string | null>(() => {
return searchParams.get('sort_by') || null;
});
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(() => {
const order = searchParams.get('sort_order');
return order === 'asc' ? 'asc' : 'desc';
});
const { accessToken } = useUserConfig();
@ -41,6 +46,11 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti
return decodeFiltersFromURL(searchParams, availableAttributes);
});
// Applied filters are the ones actually used for fetching (only updated on Apply click)
const [appliedFilters, setAppliedFilters] = useState<ActiveFilter[]>(() => {
return decodeFiltersFromURL(searchParams, availableAttributes);
});
// Load disposition codes from workflow configuration
const loadDispositionCodes = useCallback(async () => {
if (!accessToken) return;
@ -130,7 +140,7 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti
}
}, [workflowId, accessToken]);
const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[]) => {
const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[], sortByParam?: string | null, sortOrderParam?: 'asc' | 'desc') => {
const params = new URLSearchParams();
params.set('page', page.toString());
@ -143,17 +153,24 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti
}
}
// Add sort to URL if present
if (sortByParam) {
params.set('sort_by', sortByParam);
params.set('sort_order', sortOrderParam || 'desc');
}
router.push(`/workflow/${workflowId}/runs?${params.toString()}`, { scroll: false });
}, [router, workflowId]);
useEffect(() => {
fetchWorkflowRuns(currentPage, activeFilters, sortBy, sortOrder);
}, [currentPage, activeFilters, fetchWorkflowRuns, sortBy, sortOrder]);
fetchWorkflowRuns(currentPage, appliedFilters, sortBy, sortOrder);
}, [currentPage, appliedFilters, fetchWorkflowRuns, sortBy, sortOrder]);
const handleApplyFilters = useCallback(async () => {
setIsExecutingFilters(true);
setCurrentPage(1); // Reset to first page when applying filters
updatePageInUrl(1, activeFilters);
setAppliedFilters(activeFilters);
updatePageInUrl(1, activeFilters, sortBy, sortOrder);
await fetchWorkflowRuns(1, activeFilters, sortBy, sortOrder);
setIsExecutingFilters(false);
}, [activeFilters, fetchWorkflowRuns, updatePageInUrl, sortBy, sortOrder]);
@ -165,29 +182,36 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti
const handleClearFilters = useCallback(async () => {
setIsExecutingFilters(true);
setCurrentPage(1);
updatePageInUrl(1, []); // Clear filters from URL
setActiveFilters([]);
setAppliedFilters([]);
updatePageInUrl(1, [], sortBy, sortOrder); // Clear filters from URL
await fetchWorkflowRuns(1, [], sortBy, sortOrder); // Fetch all workflows without filters
setIsExecutingFilters(false);
}, [fetchWorkflowRuns, updatePageInUrl, sortBy, sortOrder]);
const handlePageChange = useCallback((page: number) => {
setCurrentPage(page);
updatePageInUrl(page, activeFilters);
}, [updatePageInUrl, activeFilters]);
updatePageInUrl(page, appliedFilters, sortBy, sortOrder);
}, [updatePageInUrl, appliedFilters, sortBy, sortOrder]);
const handleSort = useCallback((field: string) => {
// Reset to first page when sort changes
setCurrentPage(1);
const newSortBy = field;
let newSortOrder: 'asc' | 'desc' = 'desc';
if (sortBy === field) {
// Toggle order if same field
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
// New field, default to desc
setSortBy(field);
setSortOrder('desc');
newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
}
}, [sortBy]);
setSortBy(newSortBy);
setSortOrder(newSortOrder);
updatePageInUrl(1, appliedFilters, newSortBy, newSortOrder);
}, [sortBy, sortOrder, updatePageInUrl, appliedFilters]);
const handleReload = useCallback(() => {
fetchWorkflowRuns(currentPage, appliedFilters, sortBy, sortOrder);
}, [fetchWorkflowRuns, currentPage, appliedFilters, sortBy, sortOrder]);
return (
<div className="container mx-auto py-8">
@ -205,11 +229,13 @@ export function WorkflowExecutions({ workflowId, searchParams }: WorkflowExecuti
onApplyFilters={handleApplyFilters}
onClearFilters={handleClearFilters}
isExecutingFilters={isExecutingFilters}
hasAppliedFilters={appliedFilters.length > 0}
sortBy={sortBy}
sortOrder={sortOrder}
onSort={handleSort}
workflowId={workflowId}
accessToken={accessToken}
onReload={handleReload}
/>
</div>
);

View file

@ -38,6 +38,7 @@ interface FilterBuilderProps {
isExecuting?: boolean;
autoRefresh?: boolean;
onAutoRefreshChange?: (enabled: boolean) => void;
hasAppliedFilters?: boolean;
}
export const FilterBuilder: React.FC<FilterBuilderProps> = ({
@ -49,6 +50,7 @@ export const FilterBuilder: React.FC<FilterBuilderProps> = ({
isExecuting = false,
autoRefresh = false,
onAutoRefreshChange,
hasAppliedFilters = false,
}) => {
const [selectedAttribute, setSelectedAttribute] = useState<string>("");
const [expandedFilters, setExpandedFilters] = useState<Set<number>>(new Set());
@ -69,7 +71,8 @@ export const FilterBuilder: React.FC<FilterBuilderProps> = ({
if (isModifierPressed && event.key === 'Enter') {
event.preventDefault();
const allFiltersValid = activeFilters.every(f => f.isValid);
if (activeFilters.length > 0 && allFiltersValid && !isExecuting) {
const canApply = (activeFilters.length > 0 && allFiltersValid) || (activeFilters.length === 0 && hasAppliedFilters);
if (canApply && !isExecuting) {
onApplyFilters();
}
}
@ -79,7 +82,7 @@ export const FilterBuilder: React.FC<FilterBuilderProps> = ({
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [activeFilters, isExecuting, onApplyFilters]);
}, [activeFilters, isExecuting, onApplyFilters, hasAppliedFilters]);
const addFilter = useCallback((attributeId: string) => {
const attribute = availableAttributes.find(attr => attr.id === attributeId);
@ -411,7 +414,7 @@ export const FilterBuilder: React.FC<FilterBuilderProps> = ({
)}
{/* Apply Filters Button */}
{activeFilters.length > 0 && (
{(activeFilters.length > 0 || hasAppliedFilters) && (
<div className="flex justify-between items-center gap-2 pt-2">
{/* Auto-refresh toggle on the left */}
{onAutoRefreshChange && (
@ -440,7 +443,7 @@ export const FilterBuilder: React.FC<FilterBuilderProps> = ({
</Button>
<Button
onClick={onApplyFilters}
disabled={!allFiltersValid || isExecuting}
disabled={(activeFilters.length > 0 && !allFiltersValid) || isExecuting}
title={"Apply filters"}
>
{isExecuting ? "Applying..." : `Apply (${navigator.userAgent.toUpperCase().indexOf('MAC') >= 0 ? '⌘' : 'Ctrl'}+Enter)`}

View file

@ -31,15 +31,25 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
const [isExecutingFilters, setIsExecutingFilters] = useState(false);
const [accessToken, setAccessToken] = useState<string | null>(null);
// Sort state
const [sortBy, setSortBy] = useState<string | null>(null);
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
// Sort state (initialized from URL)
const [sortBy, setSortBy] = useState<string | null>(() => {
return searchParams?.get('sort_by') || null;
});
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(() => {
const order = searchParams?.get('sort_order');
return order === 'asc' ? 'asc' : 'desc';
});
// Initialize filters from URL
const [activeFilters, setActiveFilters] = useState<ActiveFilter[]>(() => {
return searchParams ? decodeFiltersFromURL(searchParams, availableAttributes) : [];
});
// Applied filters are the ones actually used for fetching (only updated on Apply click)
const [appliedFilters, setAppliedFilters] = useState<ActiveFilter[]>(() => {
return searchParams ? decodeFiltersFromURL(searchParams, availableAttributes) : [];
});
// Get access token on mount
useEffect(() => {
const fetchToken = async () => {
@ -103,7 +113,7 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
}
}, [campaignId, accessToken]);
const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[]) => {
const updatePageInUrl = useCallback((page: number, filters?: ActiveFilter[], sortByParam?: string | null, sortOrderParam?: 'asc' | 'desc') => {
const params = new URLSearchParams();
params.set('page', page.toString());
@ -116,19 +126,26 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
}
}
// Add sort to URL if present
if (sortByParam) {
params.set('sort_by', sortByParam);
params.set('sort_order', sortOrderParam || 'desc');
}
router.push(`/campaigns/${campaignId}?${params.toString()}`, { scroll: false });
}, [router, campaignId]);
useEffect(() => {
if (accessToken) {
fetchCampaignRuns(currentPage, activeFilters, sortBy, sortOrder);
fetchCampaignRuns(currentPage, appliedFilters, sortBy, sortOrder);
}
}, [currentPage, activeFilters, fetchCampaignRuns, accessToken, sortBy, sortOrder]);
}, [currentPage, appliedFilters, fetchCampaignRuns, accessToken, sortBy, sortOrder]);
const handleApplyFilters = useCallback(async () => {
setIsExecutingFilters(true);
setCurrentPage(1);
updatePageInUrl(1, activeFilters);
setAppliedFilters(activeFilters);
updatePageInUrl(1, activeFilters, sortBy, sortOrder);
await fetchCampaignRuns(1, activeFilters, sortBy, sortOrder);
setIsExecutingFilters(false);
}, [activeFilters, fetchCampaignRuns, updatePageInUrl, sortBy, sortOrder]);
@ -141,29 +158,35 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
setIsExecutingFilters(true);
setCurrentPage(1);
setActiveFilters([]);
updatePageInUrl(1, []);
setAppliedFilters([]);
updatePageInUrl(1, [], sortBy, sortOrder);
await fetchCampaignRuns(1, [], sortBy, sortOrder);
setIsExecutingFilters(false);
}, [fetchCampaignRuns, updatePageInUrl, sortBy, sortOrder]);
const handlePageChange = useCallback((page: number) => {
setCurrentPage(page);
updatePageInUrl(page, activeFilters);
}, [updatePageInUrl, activeFilters]);
updatePageInUrl(page, appliedFilters, sortBy, sortOrder);
}, [updatePageInUrl, appliedFilters, sortBy, sortOrder]);
const handleSort = useCallback((field: string) => {
// Reset to first page when sort changes
setCurrentPage(1);
const newSortBy = field;
let newSortOrder: 'asc' | 'desc' = 'desc';
if (sortBy === field) {
// Toggle order if same field
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
// New field, default to desc
setSortBy(field);
setSortOrder('desc');
newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
}
}, [sortBy]);
setSortBy(newSortBy);
setSortOrder(newSortOrder);
updatePageInUrl(1, appliedFilters, newSortBy, newSortOrder);
}, [sortBy, sortOrder, updatePageInUrl, appliedFilters]);
const handleReload = useCallback(() => {
fetchCampaignRuns(currentPage, appliedFilters, sortBy, sortOrder);
}, [fetchCampaignRuns, currentPage, appliedFilters, sortBy, sortOrder]);
// Use a subset of filter attributes relevant for campaigns
const campaignFilterAttributes: FilterAttribute[] = availableAttributes.filter(
@ -185,11 +208,13 @@ export function CampaignRuns({ campaignId, workflowId, searchParams }: CampaignR
onApplyFilters={handleApplyFilters}
onClearFilters={handleClearFilters}
isExecutingFilters={isExecutingFilters}
hasAppliedFilters={appliedFilters.length > 0}
sortBy={sortBy}
sortOrder={sortOrder}
onSort={handleSort}
workflowId={workflowId}
accessToken={accessToken}
onReload={handleReload}
title="Campaign Workflow Runs"
emptyMessage="No workflow runs found for this campaign"
/>

View file

@ -1,6 +1,6 @@
"use client";
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, ExternalLink } from "lucide-react";
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, ExternalLink, RefreshCw } from "lucide-react";
import { useState } from "react";
import { WorkflowRunResponseSchema } from "@/client/types.gen";
@ -38,6 +38,7 @@ export interface WorkflowRunsTableProps {
onApplyFilters: () => void;
onClearFilters: () => void;
isExecutingFilters: boolean;
hasAppliedFilters?: boolean;
// Sorting
sortBy?: string | null;
@ -48,6 +49,9 @@ export interface WorkflowRunsTableProps {
workflowId: number;
accessToken: string | null;
// Reload
onReload?: () => void;
// Optional customization
title?: string;
subtitle?: string;
@ -69,11 +73,13 @@ export function WorkflowRunsTable({
onApplyFilters,
onClearFilters,
isExecutingFilters,
hasAppliedFilters = false,
sortBy,
sortOrder = 'desc',
onSort,
workflowId,
accessToken,
onReload,
title = "Workflow Run History",
subtitle,
showFilters = true,
@ -103,6 +109,7 @@ export function WorkflowRunsTable({
onApplyFilters={onApplyFilters}
onClearFilters={onClearFilters}
isExecuting={isExecutingFilters}
hasAppliedFilters={hasAppliedFilters}
/>
</div>
)}
@ -123,10 +130,25 @@ export function WorkflowRunsTable({
) : (
<Card>
<CardHeader>
<CardTitle>Workflow Runs</CardTitle>
<CardDescription>
{subtitle || `Showing ${runs.length} of ${totalCount} total runs`}
</CardDescription>
<div className="flex items-center justify-between">
<div>
<CardTitle>Workflow Runs</CardTitle>
<CardDescription>
{subtitle || `Showing ${runs.length} of ${totalCount} total runs`}
</CardDescription>
</div>
{onReload && (
<Button
variant="outline"
size="icon"
onClick={onReload}
disabled={loading}
title="Reload"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
)}
</div>
</CardHeader>
<CardContent>
<div className="bg-card border border-border rounded-lg overflow-hidden shadow-sm">