mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-11 22:51:26 +00:00
Move pagination to the server side (#166)
This commit is contained in:
parent
9689177a70
commit
9a94ad70bf
3 changed files with 202 additions and 18 deletions
157
apps/web/src/actions/analytics/__tests__/events.test.ts
Normal file
157
apps/web/src/actions/analytics/__tests__/events.test.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getTasks } from '../events';
|
||||
|
||||
// Mock the analytics module
|
||||
vi.mock('@/lib/server', () => ({
|
||||
analytics: {
|
||||
query: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the auth module
|
||||
vi.mock('@/actions/auth', () => ({
|
||||
authorizeAnalytics: vi.fn().mockResolvedValue({
|
||||
effectiveUserId: 'test-user-id',
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the db module
|
||||
vi.mock('@roo-code-cloud/db/server', () => ({
|
||||
getUsersById: vi.fn().mockResolvedValue({
|
||||
'test-user-id': {
|
||||
id: 'test-user-id',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('getTasks with filters', () => {
|
||||
let mockQuery: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const { analytics } = await import('@/lib/server');
|
||||
mockQuery = vi.mocked(analytics.query);
|
||||
mockQuery.mockResolvedValue({
|
||||
json: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
taskId: 'task-1',
|
||||
userId: 'test-user-id',
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
mode: 'code',
|
||||
completed: true,
|
||||
tokens: 1000,
|
||||
cost: 0.02,
|
||||
timestamp: 1640995200,
|
||||
title: 'Test Task',
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
repositoryName: 'test-repo',
|
||||
defaultBranch: 'main',
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include userId filter in query parameters', async () => {
|
||||
await getTasks({
|
||||
orgId: 'test-org',
|
||||
filterType: 'userId',
|
||||
filterValue: 'filter-user-id',
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query_params: expect.objectContaining({
|
||||
filterUserId: 'filter-user-id',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const queryCall = mockQuery.mock.calls[0]?.[0];
|
||||
expect(queryCall?.query).toContain('AND e.userId = {filterUserId: String}');
|
||||
});
|
||||
|
||||
it('should include model filter in query parameters', async () => {
|
||||
await getTasks({
|
||||
orgId: 'test-org',
|
||||
filterType: 'model',
|
||||
filterValue: 'gpt-4',
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query_params: expect.objectContaining({
|
||||
filterModel: 'gpt-4',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const queryCall = mockQuery.mock.calls[0]?.[0];
|
||||
expect(queryCall?.query).toContain('AND e.modelId = {filterModel: String}');
|
||||
});
|
||||
|
||||
it('should include repository filter in query parameters', async () => {
|
||||
await getTasks({
|
||||
orgId: 'test-org',
|
||||
filterType: 'repositoryName',
|
||||
filterValue: 'test-repo',
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
expect(mockQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query_params: expect.objectContaining({
|
||||
filterRepository: 'test-repo',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const queryCall = mockQuery.mock.calls[0]?.[0];
|
||||
expect(queryCall?.query).toContain(
|
||||
'AND e.repositoryName = {filterRepository: String}',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not include filter clauses when no filter is provided', async () => {
|
||||
await getTasks({
|
||||
orgId: 'test-org',
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
const queryCall = mockQuery.mock.calls[0]?.[0];
|
||||
expect(queryCall?.query).not.toContain('filterUserId');
|
||||
expect(queryCall?.query).not.toContain('filterModel');
|
||||
expect(queryCall?.query).not.toContain('filterRepository');
|
||||
});
|
||||
|
||||
it('should return properly formatted tasks with user data', async () => {
|
||||
const result = await getTasks({
|
||||
orgId: 'test-org',
|
||||
filterType: 'model',
|
||||
filterValue: 'gpt-4',
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
tasks: [
|
||||
expect.objectContaining({
|
||||
taskId: 'task-1',
|
||||
userId: 'test-user-id',
|
||||
model: 'gpt-4',
|
||||
user: expect.objectContaining({
|
||||
id: 'test-user-id',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
hasMore: false,
|
||||
nextCursor: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -409,6 +409,8 @@ export const getTasks = async ({
|
|||
skipAuth = false,
|
||||
limit = 20,
|
||||
cursor,
|
||||
filterType,
|
||||
filterValue,
|
||||
}: {
|
||||
orgId?: string | null;
|
||||
userId?: string | null;
|
||||
|
|
@ -417,6 +419,8 @@ export const getTasks = async ({
|
|||
skipAuth?: boolean;
|
||||
limit?: number;
|
||||
cursor?: number;
|
||||
filterType?: 'userId' | 'model' | 'repositoryName';
|
||||
filterValue?: string;
|
||||
}): Promise<TasksResult> => {
|
||||
let effectiveUserId = userId;
|
||||
|
||||
|
|
@ -476,6 +480,28 @@ export const getTasks = async ({
|
|||
queryParams.cursor = cursor;
|
||||
}
|
||||
|
||||
// Add filter parameters
|
||||
if (filterType && filterValue) {
|
||||
if (filterType === 'userId') {
|
||||
queryParams.filterUserId = filterValue;
|
||||
} else if (filterType === 'model') {
|
||||
queryParams.filterModel = filterValue;
|
||||
} else if (filterType === 'repositoryName') {
|
||||
queryParams.filterRepository = filterValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Build filter conditions
|
||||
const filterConditions = [];
|
||||
if (filterType === 'userId' && filterValue) {
|
||||
filterConditions.push('AND e.userId = {filterUserId: String}');
|
||||
} else if (filterType === 'model' && filterValue) {
|
||||
filterConditions.push('AND e.modelId = {filterModel: String}');
|
||||
} else if (filterType === 'repositoryName' && filterValue) {
|
||||
filterConditions.push('AND e.repositoryName = {filterRepository: String}');
|
||||
}
|
||||
const filterClause = filterConditions.join(' ');
|
||||
|
||||
// TODO: Handle same-timestamp edge cases
|
||||
// Currently using only timestamp as cursor, but this can miss/duplicate tasks
|
||||
// if multiple tasks have the same timestamp at page boundaries.
|
||||
|
|
@ -519,6 +545,7 @@ export const getTasks = async ({
|
|||
AND e.modelId IS NOT NULL
|
||||
${userFilter}
|
||||
${taskFilter}
|
||||
${filterClause}
|
||||
GROUP BY 1, 2
|
||||
${havingFilter}
|
||||
ORDER BY timestamp DESC
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAuth } from '@clerk/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
|
|
@ -40,6 +40,8 @@ export const Tasks = ({
|
|||
!orgId,
|
||||
pagination.currentCursor,
|
||||
pagination.pageSize,
|
||||
filter?.type,
|
||||
filter?.value,
|
||||
],
|
||||
queryFn: () =>
|
||||
getTasks({
|
||||
|
|
@ -47,6 +49,8 @@ export const Tasks = ({
|
|||
userId: userRole === 'member' ? currentUserId : undefined,
|
||||
limit: pagination.pageSize,
|
||||
cursor: pagination.currentCursor,
|
||||
filterType: filter?.type,
|
||||
filterValue: filter?.value,
|
||||
}),
|
||||
enabled: true, // Run for both personal and organization context
|
||||
...polling,
|
||||
|
|
@ -59,26 +63,22 @@ export const Tasks = ({
|
|||
}
|
||||
}, [data?.nextCursor, pagination]);
|
||||
|
||||
// Note: The pagination hook automatically handles total updates via the pagination controls
|
||||
// Reset pagination when filter changes
|
||||
const prevFilter = useRef<Filter | null>(null);
|
||||
useEffect(() => {
|
||||
const currentFilter = filter;
|
||||
const hasFilterChanged =
|
||||
prevFilter.current?.type !== currentFilter?.type ||
|
||||
prevFilter.current?.value !== currentFilter?.value;
|
||||
|
||||
const tasks = useMemo(() => {
|
||||
const allTasks = data?.tasks || [];
|
||||
|
||||
if (!filter) {
|
||||
return allTasks;
|
||||
if (hasFilterChanged) {
|
||||
pagination.reset();
|
||||
prevFilter.current = currentFilter;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [filter]);
|
||||
|
||||
return allTasks.filter((task) => {
|
||||
if (filter.type === 'userId') {
|
||||
return task.userId === filter.value;
|
||||
} else if (filter.type === 'model') {
|
||||
return task.model === filter.value;
|
||||
} else if (filter.type === 'repositoryName') {
|
||||
return task.repositoryName === filter.value;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}, [filter, data?.tasks]);
|
||||
const tasks = data?.tasks || [];
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue