mirror of https://github.com/kortix-ai/suna.git
Merge pull request #1797 from escapade-mckv/show-all-runs-for-admin
show paginated details in admin dialog
This commit is contained in:
commit
8d17f779b0
|
@ -314,6 +314,78 @@ async def get_user_stats_overview(
|
||||||
logger.error(f"Failed to get user stats: {e}", exc_info=True)
|
logger.error(f"Failed to get user stats: {e}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail="Failed to retrieve user statistics")
|
raise HTTPException(status_code=500, detail="Failed to retrieve user statistics")
|
||||||
|
|
||||||
|
@router.get("/users/{user_id}/activity")
|
||||||
|
async def get_user_activity(
|
||||||
|
user_id: str,
|
||||||
|
page: int = Query(1, ge=1, description="Page number"),
|
||||||
|
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
|
||||||
|
status_filter: Optional[str] = Query(None, description="Filter by status"),
|
||||||
|
admin: dict = Depends(require_admin)
|
||||||
|
):
|
||||||
|
"""Get paginated activity (agent runs) for a specific user."""
|
||||||
|
try:
|
||||||
|
db = DBConnection()
|
||||||
|
client = await db.client
|
||||||
|
|
||||||
|
pagination_params = PaginationParams(page=page, page_size=page_size)
|
||||||
|
|
||||||
|
# Build base query for agent runs with thread info
|
||||||
|
base_query = client.from_('agent_runs').select(
|
||||||
|
'*, threads!inner(account_id, thread_id)'
|
||||||
|
).eq('threads.account_id', user_id)
|
||||||
|
|
||||||
|
if status_filter:
|
||||||
|
base_query = base_query.eq('status', status_filter)
|
||||||
|
|
||||||
|
# Get total count
|
||||||
|
count_result = await client.from_('agent_runs').select(
|
||||||
|
'id, threads!inner(account_id)', count='exact'
|
||||||
|
).eq('threads.account_id', user_id).execute()
|
||||||
|
|
||||||
|
total_count = count_result.count or 0
|
||||||
|
|
||||||
|
# Get paginated activity
|
||||||
|
offset = (pagination_params.page - 1) * pagination_params.page_size
|
||||||
|
activity_query = client.from_('agent_runs').select(
|
||||||
|
'*, threads!inner(account_id, thread_id)'
|
||||||
|
).eq('threads.account_id', user_id)
|
||||||
|
|
||||||
|
if status_filter:
|
||||||
|
activity_query = activity_query.eq('status', status_filter)
|
||||||
|
|
||||||
|
activity_result = await activity_query.order('created_at', desc=True).range(
|
||||||
|
offset, offset + pagination_params.page_size - 1
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
# Format activity data
|
||||||
|
activities = []
|
||||||
|
for run in activity_result.data or []:
|
||||||
|
thread = run.get('threads', {})
|
||||||
|
|
||||||
|
activities.append({
|
||||||
|
'id': run.get('id'),
|
||||||
|
'created_at': run.get('created_at'),
|
||||||
|
'updated_at': run.get('updated_at'),
|
||||||
|
'status': run.get('status'),
|
||||||
|
'thread_id': run.get('thread_id'),
|
||||||
|
'thread_name': f"Thread {run.get('thread_id', '').split('-')[0] if run.get('thread_id') else 'Unknown'}",
|
||||||
|
'agent_id': run.get('agent_id'),
|
||||||
|
'agent_name': 'Agent', # We'll need to fetch agent names separately if needed
|
||||||
|
'credit_cost': float(run.get('credit_cost', 0) if run.get('credit_cost') else 0),
|
||||||
|
'error': run.get('error'),
|
||||||
|
'duration_ms': run.get('duration_ms')
|
||||||
|
})
|
||||||
|
|
||||||
|
return await PaginationService.paginate_with_total_count(
|
||||||
|
items=activities,
|
||||||
|
total_count=total_count,
|
||||||
|
params=pagination_params
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get user activity: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to retrieve user activity")
|
||||||
|
|
||||||
@router.get("/users/threads/by-email")
|
@router.get("/users/threads/by-email")
|
||||||
async def get_user_threads_by_email(
|
async def get_user_threads_by_email(
|
||||||
email: str = Query(..., description="User email to fetch threads for"),
|
email: str = Query(..., description="User email to fetch threads for"),
|
||||||
|
|
|
@ -4,7 +4,7 @@ Handles all administrative billing operations: credits, refunds, transactions.
|
||||||
User search has been moved to admin_api.py as it's user-focused, not billing-focused.
|
User search has been moved to admin_api.py as it's user-focused, not billing-focused.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
|
@ -185,31 +185,60 @@ async def get_user_billing_summary(
|
||||||
@router.get("/user/{account_id}/transactions")
|
@router.get("/user/{account_id}/transactions")
|
||||||
async def get_user_transactions(
|
async def get_user_transactions(
|
||||||
account_id: str,
|
account_id: str,
|
||||||
limit: int = 100,
|
page: int = Query(1, ge=1, description="Page number"),
|
||||||
offset: int = 0,
|
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
|
||||||
type_filter: Optional[str] = None,
|
type_filter: Optional[str] = None,
|
||||||
admin: dict = Depends(require_admin)
|
admin: dict = Depends(require_admin)
|
||||||
):
|
):
|
||||||
"""Get transaction history for a specific user."""
|
try:
|
||||||
db = DBConnection()
|
from core.utils.pagination import PaginationService, PaginationParams
|
||||||
client = await db.client
|
|
||||||
|
db = DBConnection()
|
||||||
query = client.from_('credit_ledger').select('*').eq('account_id', account_id).order('created_at', desc=True)
|
client = await db.client
|
||||||
|
|
||||||
if type_filter:
|
pagination_params = PaginationParams(page=page, page_size=page_size)
|
||||||
query = query.eq('type', type_filter)
|
|
||||||
|
# Get total count
|
||||||
if offset:
|
count_query = client.from_('credit_ledger').select('*', count='exact').eq('account_id', account_id)
|
||||||
query = query.range(offset, offset + limit - 1)
|
if type_filter:
|
||||||
else:
|
count_query = count_query.eq('type', type_filter)
|
||||||
query = query.limit(limit)
|
count_result = await count_query.execute()
|
||||||
|
total_count = count_result.count or 0
|
||||||
transactions_result = await query.execute()
|
|
||||||
|
# Get paginated transactions
|
||||||
return {
|
offset = (pagination_params.page - 1) * pagination_params.page_size
|
||||||
'account_id': account_id,
|
transactions_query = client.from_('credit_ledger').select('*').eq('account_id', account_id)
|
||||||
'transactions': transactions_result.data or [],
|
|
||||||
'count': len(transactions_result.data or [])
|
if type_filter:
|
||||||
}
|
transactions_query = transactions_query.eq('type', type_filter)
|
||||||
|
|
||||||
|
transactions_result = await transactions_query.order('created_at', desc=True).range(
|
||||||
|
offset, offset + pagination_params.page_size - 1
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
# Format transactions
|
||||||
|
transactions = []
|
||||||
|
for tx in transactions_result.data or []:
|
||||||
|
transactions.append({
|
||||||
|
'id': tx.get('id'),
|
||||||
|
'created_at': tx.get('created_at'),
|
||||||
|
'amount': float(tx.get('amount', 0)),
|
||||||
|
'balance_after': float(tx.get('balance_after', 0)),
|
||||||
|
'type': tx.get('type'),
|
||||||
|
'description': tx.get('description'),
|
||||||
|
'is_expiring': tx.get('is_expiring', False),
|
||||||
|
'expires_at': tx.get('expires_at'),
|
||||||
|
'metadata': tx.get('metadata', {})
|
||||||
|
})
|
||||||
|
|
||||||
|
return await PaginationService.paginate_with_total_count(
|
||||||
|
items=transactions,
|
||||||
|
total_count=total_count,
|
||||||
|
params=pagination_params
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get user transactions: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="Failed to retrieve transactions")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -40,11 +40,12 @@ import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAdminUserDetails, useAdminUserThreads } from '@/hooks/react-query/admin/use-admin-users';
|
import { useAdminUserDetails, useAdminUserThreads, useAdminUserActivity } from '@/hooks/react-query/admin/use-admin-users';
|
||||||
import {
|
import {
|
||||||
useUserBillingSummary,
|
useUserBillingSummary,
|
||||||
useAdjustCredits,
|
useAdjustCredits,
|
||||||
useProcessRefund,
|
useProcessRefund,
|
||||||
|
useAdminUserTransactions,
|
||||||
} from '@/hooks/react-query/admin/use-admin-billing';
|
} from '@/hooks/react-query/admin/use-admin-billing';
|
||||||
import type { UserSummary } from '@/hooks/react-query/admin/use-admin-users';
|
import type { UserSummary } from '@/hooks/react-query/admin/use-admin-users';
|
||||||
|
|
||||||
|
@ -68,6 +69,8 @@ export function AdminUserDetailsDialog({
|
||||||
const [adjustIsExpiring, setAdjustIsExpiring] = useState(true);
|
const [adjustIsExpiring, setAdjustIsExpiring] = useState(true);
|
||||||
const [refundIsExpiring, setRefundIsExpiring] = useState(false);
|
const [refundIsExpiring, setRefundIsExpiring] = useState(false);
|
||||||
const [threadsPage, setThreadsPage] = useState(1);
|
const [threadsPage, setThreadsPage] = useState(1);
|
||||||
|
const [transactionsPage, setTransactionsPage] = useState(1);
|
||||||
|
const [activityPage, setActivityPage] = useState(1);
|
||||||
|
|
||||||
const { data: userDetails, isLoading } = useAdminUserDetails(user?.id || null);
|
const { data: userDetails, isLoading } = useAdminUserDetails(user?.id || null);
|
||||||
const { data: billingSummary, refetch: refetchBilling } = useUserBillingSummary(user?.id || null);
|
const { data: billingSummary, refetch: refetchBilling } = useUserBillingSummary(user?.id || null);
|
||||||
|
@ -76,6 +79,16 @@ export function AdminUserDetailsDialog({
|
||||||
page: threadsPage,
|
page: threadsPage,
|
||||||
page_size: 10,
|
page_size: 10,
|
||||||
});
|
});
|
||||||
|
const { data: userTransactions, isLoading: transactionsLoading } = useAdminUserTransactions({
|
||||||
|
userId: user?.id || '',
|
||||||
|
page: transactionsPage,
|
||||||
|
page_size: 10,
|
||||||
|
});
|
||||||
|
const { data: userActivity, isLoading: activityLoading } = useAdminUserActivity({
|
||||||
|
userId: user?.id || '',
|
||||||
|
page: activityPage,
|
||||||
|
page_size: 10,
|
||||||
|
});
|
||||||
const adjustCreditsMutation = useAdjustCredits();
|
const adjustCreditsMutation = useAdjustCredits();
|
||||||
const processRefundMutation = useProcessRefund();
|
const processRefundMutation = useProcessRefund();
|
||||||
|
|
||||||
|
@ -385,40 +398,73 @@ export function AdminUserDetailsDialog({
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="transactions" className="space-y-4">
|
<TabsContent value="transactions" className="space-y-4">
|
||||||
{billingSummary && (
|
<Card>
|
||||||
<Card className='border-0 shadow-none bg-transparent'>
|
<CardHeader>
|
||||||
<CardContent className='p-0'>
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<DollarSign className="h-4 w-4" />
|
||||||
|
Transactions
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{transactionsLoading ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{billingSummary.recent_transactions?.length > 0 ? (
|
{[...Array(3)].map((_, i) => (
|
||||||
billingSummary.recent_transactions.map((transaction: any) => (
|
<Skeleton key={i} className="h-16 w-full" />
|
||||||
<div
|
))}
|
||||||
key={transaction.id}
|
</div>
|
||||||
className="flex items-center justify-between p-3 border rounded-lg"
|
) : userTransactions && userTransactions.data?.length > 0 ? (
|
||||||
>
|
<div className="space-y-2">
|
||||||
<div>
|
{userTransactions.data.map((transaction: any) => (
|
||||||
<p className="text-sm font-medium">{transaction.description}</p>
|
<div
|
||||||
<p className="text-xs text-muted-foreground">
|
key={transaction.id}
|
||||||
{formatDate(transaction.created_at)}
|
className="flex items-center justify-between p-3 border rounded-lg"
|
||||||
</p>
|
>
|
||||||
</div>
|
<div>
|
||||||
<div className="text-right">
|
<p className="text-sm font-medium">{transaction.description}</p>
|
||||||
<p className={`font-semibold ${getTransactionColor(transaction.type)}`}>
|
<p className="text-xs text-muted-foreground">
|
||||||
{transaction.amount > 0 ? '+' : ''}
|
{formatDate(transaction.created_at)}
|
||||||
{formatCurrency(Math.abs(transaction.amount))}
|
</p>
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Balance: {formatCurrency(transaction.balance_after)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
<div className="text-right">
|
||||||
) : (
|
<p className={`font-semibold ${getTransactionColor(transaction.type)}`}>
|
||||||
<p className="text-sm text-muted-foreground">No recent transactions</p>
|
{transaction.amount > 0 ? '+' : ''}
|
||||||
|
{formatCurrency(Math.abs(transaction.amount))}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Balance: {formatCurrency(transaction.balance_after)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{userTransactions.pagination && userTransactions.pagination.total_pages > 1 && (
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!userTransactions.pagination.has_prev}
|
||||||
|
onClick={() => setTransactionsPage(p => Math.max(1, p - 1))}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Page {userTransactions.pagination.page} of {userTransactions.pagination.total_pages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!userTransactions.pagination.has_next}
|
||||||
|
onClick={() => setTransactionsPage(p => p + 1)}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
) : (
|
||||||
</Card>
|
<p className="text-sm text-muted-foreground">No transactions found</p>
|
||||||
)}
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="activity" className="space-y-4">
|
<TabsContent value="activity" className="space-y-4">
|
||||||
|
@ -426,33 +472,77 @@ export function AdminUserDetailsDialog({
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Activity className="h-4 w-4" />
|
<Activity className="h-4 w-4" />
|
||||||
Recent Activity
|
Activity
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{userDetails?.recent_activity?.length > 0 ? (
|
{activityLoading ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{userDetails.recent_activity.map((activity) => (
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-16 w-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : userActivity && userActivity.data?.length > 0 ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{userActivity.data.map((activity: any) => (
|
||||||
<div
|
<div
|
||||||
key={activity.id}
|
key={activity.id}
|
||||||
className="flex items-center justify-between p-3 border rounded-lg"
|
className="flex items-center justify-between p-3 border rounded-lg"
|
||||||
>
|
>
|
||||||
<div>
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium">Agent Run</p>
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-sm font-medium">{activity.agent_name}</p>
|
||||||
{formatDate(activity.created_at)} • Thread {activity.thread_id.slice(-8)}
|
<Badge
|
||||||
|
variant={activity.status === 'completed' ? 'default' : activity.status === 'failed' ? 'destructive' : 'secondary'}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{activity.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{formatDate(activity.created_at)} • Thread: {activity.thread_name || activity.thread_id.slice(-8)}
|
||||||
</p>
|
</p>
|
||||||
|
{activity.error && (
|
||||||
|
<p className="text-xs text-red-600 mt-1 truncate">
|
||||||
|
Error: {activity.error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
{activity.credit_cost > 0 && (
|
||||||
variant={activity.status === 'completed' ? 'default' : 'secondary'}
|
<div className="text-right ml-2">
|
||||||
>
|
<p className="text-sm font-medium text-muted-foreground">
|
||||||
{activity.status}
|
{formatCurrency(activity.credit_cost)}
|
||||||
</Badge>
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{userActivity.pagination && userActivity.pagination.total_pages > 1 && (
|
||||||
|
<div className="flex items-center justify-between pt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!userActivity.pagination.has_prev}
|
||||||
|
onClick={() => setActivityPage(p => Math.max(1, p - 1))}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Page {userActivity.pagination.page} of {userActivity.pagination.total_pages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!userActivity.pagination.has_next}
|
||||||
|
onClick={() => setActivityPage(p => p + 1)}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-muted-foreground">No recent activity</p>
|
<p className="text-sm text-muted-foreground">No activity found</p>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -33,24 +33,31 @@ export function useUserBillingSummary(userId: string | null) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUserTransactions(userId: string | null, limit = 100, offset = 0, typeFilter?: string) {
|
interface TransactionParams {
|
||||||
|
userId: string;
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
type_filter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAdminUserTransactions(params: TransactionParams) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['admin', 'billing', 'transactions', userId, limit, offset, typeFilter],
|
queryKey: ['admin', 'billing', 'transactions', params.userId, params.page, params.page_size, params.type_filter],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!userId) return null;
|
const searchParams = new URLSearchParams();
|
||||||
const params = new URLSearchParams({
|
|
||||||
limit: limit.toString(),
|
|
||||||
offset: offset.toString(),
|
|
||||||
});
|
|
||||||
if (typeFilter) params.append('type_filter', typeFilter);
|
|
||||||
|
|
||||||
const response = await backendApi.get(`/admin/billing/user/${userId}/transactions?${params}`);
|
if (params.page) searchParams.append('page', params.page.toString());
|
||||||
|
if (params.page_size) searchParams.append('page_size', params.page_size.toString());
|
||||||
|
if (params.type_filter) searchParams.append('type_filter', params.type_filter);
|
||||||
|
|
||||||
|
const response = await backendApi.get(`/admin/billing/user/${params.userId}/transactions?${searchParams.toString()}`);
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
throw new Error(response.error.message);
|
throw new Error(response.error.message);
|
||||||
}
|
}
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
enabled: !!userId,
|
enabled: !!params.userId,
|
||||||
|
staleTime: 30000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -184,6 +184,34 @@ export function useAdminUserThreads(params: UserThreadsParams) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UserActivityParams {
|
||||||
|
userId: string;
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
status_filter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAdminUserActivity(params: UserActivityParams) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['admin', 'users', 'activity', params.userId, params.page, params.page_size, params.status_filter],
|
||||||
|
queryFn: async () => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (params.page) searchParams.append('page', params.page.toString());
|
||||||
|
if (params.page_size) searchParams.append('page_size', params.page_size.toString());
|
||||||
|
if (params.status_filter) searchParams.append('status_filter', params.status_filter);
|
||||||
|
|
||||||
|
const response = await backendApi.get(`/admin/users/${params.userId}/activity?${searchParams.toString()}`);
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
enabled: !!params.userId,
|
||||||
|
staleTime: 30000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useRefreshUserData() {
|
export function useRefreshUserData() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue