diff --git a/apps/rowboat/app/actions/job_actions.ts b/apps/rowboat/app/actions/job_actions.ts index 69d4ca16..acf4ac24 100644 --- a/apps/rowboat/app/actions/job_actions.ts +++ b/apps/rowboat/app/actions/job_actions.ts @@ -4,12 +4,15 @@ import { container } from "@/di/container"; import { IListJobsController } from "@/src/interface-adapters/controllers/jobs/list-jobs.controller"; import { IFetchJobController } from "@/src/interface-adapters/controllers/jobs/fetch-job.controller"; import { authCheck } from "./auth_actions"; +import { JobFiltersSchema } from "@/src/application/repositories/jobs.repository.interface"; +import { z } from "zod"; const listJobsController = container.resolve('listJobsController'); const fetchJobController = container.resolve('fetchJobController'); export async function listJobs(request: { projectId: string, + filters?: z.infer, cursor?: string, limit?: number, }) { @@ -19,6 +22,7 @@ export async function listJobs(request: { caller: 'user', userId: user._id, projectId: request.projectId, + filters: request.filters, cursor: request.cursor, limit: request.limit, }); diff --git a/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rule-view.tsx b/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rule-view.tsx index e62fe808..f5eba5f4 100644 --- a/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rule-view.tsx +++ b/apps/rowboat/app/projects/[projectId]/job-rules/components/recurring-job-rule-view.tsx @@ -10,6 +10,7 @@ import Link from "next/link"; import { RecurringJobRule } from "@/src/entities/models/recurring-job-rule"; import { Spinner } from "@heroui/react"; import { z } from "zod"; +import { JobsList } from "@/app/projects/[projectId]/jobs/components/jobs-list"; export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string; ruleId: string }) { const router = useRouter(); @@ -263,6 +264,18 @@ export function RecurringJobRuleView({ projectId, ruleId }: { projectId: string;
Rule ID: {rule.id}
+ + {/* Jobs Created by This Rule */} +
+

+ Jobs Created by This Rule +

+ +
diff --git a/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx b/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx index 3bc5885d..19a671ad 100644 --- a/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx +++ b/apps/rowboat/app/projects/[projectId]/jobs/components/jobs-list.tsx @@ -6,12 +6,19 @@ import { Button } from "@/components/ui/button"; import { Panel } from "@/components/common/panel-common"; import { listJobs } from "@/app/actions/job_actions"; import { z } from "zod"; -import { ListedJobItem } from "@/src/application/repositories/jobs.repository.interface"; +import { ListedJobItem, JobFilters } from "@/src/application/repositories/jobs.repository.interface"; import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date"; type ListedItem = z.infer; -export function JobsList({ projectId }: { projectId: string }) { +interface JobsListProps { + projectId: string; + filters?: JobFilters; + showTitle?: boolean; + customTitle?: string; +} + +export function JobsList({ projectId, filters, showTitle = true, customTitle }: JobsListProps) { const [items, setItems] = useState([]); const [cursor, setCursor] = useState(null); const [loading, setLoading] = useState(true); @@ -19,14 +26,22 @@ export function JobsList({ projectId }: { projectId: string }) { const [hasMore, setHasMore] = useState(false); const fetchPage = useCallback(async (cursorArg?: string | null) => { - const res = await listJobs({ projectId, cursor: cursorArg ?? undefined, limit: 20 }); + const res = await listJobs({ + projectId, + filters, + cursor: cursorArg ?? undefined, + limit: 20 + }); return res; - }, [projectId]); + }, [projectId, filters]); useEffect(() => { let ignore = false; (async () => { setLoading(true); + setItems([]); + setCursor(null); + setHasMore(false); const res = await fetchPage(null); if (ignore) return; setItems(res.items); @@ -35,7 +50,7 @@ export function JobsList({ projectId }: { projectId: string }) { setLoading(false); })(); return () => { ignore = true; }; - }, [fetchPage]); + }, [fetchPage, filters]); const loadMore = useCallback(async () => { if (!cursor) return; @@ -111,14 +126,21 @@ export function JobsList({ projectId }: { projectId: string }) { return ( -
- JOBS + showTitle ? ( +
+
+ {customTitle || "JOBS"} +
-
+ ) : null } rightActions={
+ {filters && items.length > 0 && ( +
+ {items.length} job{items.length !== 1 ? 's' : ''} found +
+ )} {/* Reserved for future actions */}
} @@ -132,7 +154,9 @@ export function JobsList({ projectId }: { projectId: string }) { )} {!loading && items.length === 0 && ( -

No jobs yet.

+

+ {filters ? "No jobs found matching the current filters." : "No jobs yet."} +

)} {!loading && items.length > 0 && (
diff --git a/apps/rowboat/src/application/repositories/jobs.repository.interface.ts b/apps/rowboat/src/application/repositories/jobs.repository.interface.ts index 465cba4d..93b07747 100644 --- a/apps/rowboat/src/application/repositories/jobs.repository.interface.ts +++ b/apps/rowboat/src/application/repositories/jobs.repository.interface.ts @@ -23,6 +23,31 @@ export const ListedJobItem = Job.pick({ updatedAt: true, }); +/** + * Schema for filtering jobs when listing. + * This schema is designed to be extensible for future filtering criteria. + */ +export const JobFiltersSchema = z.object({ + // Filter by job status + status: z.enum(["pending", "running", "completed", "failed"]).optional(), + + // Filter by recurring job rule ID + recurringJobRuleId: z.string().optional(), + + // Filter by composio trigger deployment ID + composioTriggerDeploymentId: z.string().optional(), + + // Filter by date range + createdAfter: z.string().datetime().optional(), + createdBefore: z.string().datetime().optional(), + + // Extensible: add more filters here as needed + // Example: errorMessage: z.string().optional(), + // Example: priority: z.enum(["low", "medium", "high"]).optional(), +}).strict(); + +export type JobFilters = z.infer; + /** * Schema for updating an existing job. * Defines the fields that can be updated for a job. @@ -102,12 +127,18 @@ export interface IJobsRepository { release(id: string): Promise; /** - * Lists jobs for a specific project with pagination. + * Lists jobs for a specific project with optional filtering and pagination. * * @param projectId - The unique identifier of the project + * @param filters - Optional filters to apply to the job list * @param cursor - Optional cursor for pagination * @param limit - Maximum number of jobs to return (default: 50) * @returns Promise resolving to a paginated list of jobs */ - list(projectId: string, cursor?: string, limit?: number): Promise>>>; + list( + projectId: string, + filters?: JobFilters, + cursor?: string, + limit?: number + ): Promise>>>; } \ No newline at end of file diff --git a/apps/rowboat/src/application/use-cases/jobs/list-jobs.use-case.ts b/apps/rowboat/src/application/use-cases/jobs/list-jobs.use-case.ts index 746ea281..778aebb3 100644 --- a/apps/rowboat/src/application/use-cases/jobs/list-jobs.use-case.ts +++ b/apps/rowboat/src/application/use-cases/jobs/list-jobs.use-case.ts @@ -2,7 +2,7 @@ import { BadRequestError, NotFoundError } from '@/src/entities/errors/common'; import { z } from "zod"; import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface'; import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy'; -import { IJobsRepository, ListedJobItem } from '../../repositories/jobs.repository.interface'; +import { IJobsRepository, ListedJobItem, JobFilters, JobFiltersSchema } from '../../repositories/jobs.repository.interface'; import { Job } from '@/src/entities/models/job'; import { PaginatedList } from '@/src/entities/common/paginated-list'; @@ -11,6 +11,7 @@ const inputSchema = z.object({ userId: z.string().optional(), apiKey: z.string().optional(), projectId: z.string(), + filters: JobFiltersSchema.optional(), cursor: z.string().optional(), limit: z.number().optional(), }); @@ -54,6 +55,6 @@ export class ListJobsUseCase implements IListJobsUseCase { await this.usageQuotaPolicy.assertAndConsume(projectId); // fetch jobs for project - return await this.jobsRepository.list(projectId, request.cursor, limit); + return await this.jobsRepository.list(projectId, request.filters, request.cursor, limit); } } diff --git a/apps/rowboat/src/infrastructure/repositories/mongodb.jobs.repository.ts b/apps/rowboat/src/infrastructure/repositories/mongodb.jobs.repository.ts index 51df938f..c3fcf024 100644 --- a/apps/rowboat/src/infrastructure/repositories/mongodb.jobs.repository.ts +++ b/apps/rowboat/src/infrastructure/repositories/mongodb.jobs.repository.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { ObjectId } from "mongodb"; import { db } from "@/app/lib/mongodb"; -import { CreateJobSchema, IJobsRepository, ListedJobItem, UpdateJobSchema } from "@/src/application/repositories/jobs.repository.interface"; +import { CreateJobSchema, IJobsRepository, ListedJobItem, UpdateJobSchema, JobFilters } from "@/src/application/repositories/jobs.repository.interface"; import { Job } from "@/src/entities/models/job"; import { JobAcquisitionError } from "@/src/entities/errors/job-errors"; import { NotFoundError } from "@/src/entities/errors/common"; @@ -198,11 +198,43 @@ export class MongoDBJobsRepository implements IJobsRepository { } /** - * Lists jobs for a specific project with pagination. + * Lists jobs for a specific project with optional filtering and pagination. */ - async list(projectId: string, cursor?: string, limit: number = 50): Promise>>> { + async list( + projectId: string, + filters?: JobFilters, + cursor?: string, + limit: number = 50 + ): Promise>>> { const query: any = { projectId }; + const _limit = Math.min(limit, 50); + + // Apply filters if provided + if (filters) { + if (filters.status) { + query.status = filters.status; + } + + if (filters.recurringJobRuleId) { + query["reason.type"] = "recurring_job_rule"; + query["reason.ruleId"] = filters.recurringJobRuleId; + } + + if (filters.composioTriggerDeploymentId) { + query["reason.type"] = "composio_trigger"; + query["reason.triggerDeploymentId"] = filters.composioTriggerDeploymentId; + } + + if (filters.createdAfter) { + query.createdAt = { ...query.createdAt, $gte: filters.createdAfter }; + } + + if (filters.createdBefore) { + query.createdAt = { ...query.createdAt, $lte: filters.createdBefore }; + } + } + if (cursor) { query._id = { $lt: new ObjectId(cursor) }; } @@ -210,7 +242,7 @@ export class MongoDBJobsRepository implements IJobsRepository { const results = await this.collection .find(query) .sort({ _id: -1 }) - .limit(limit + 1) // Fetch one extra to determine if there's a next page + .limit(_limit + 1) // Fetch one extra to determine if there's a next page .project & { _id: ObjectId }>({ _id: 1, projectId: 1, @@ -221,8 +253,8 @@ export class MongoDBJobsRepository implements IJobsRepository { }) .toArray(); - const hasNextPage = results.length > limit; - const items = results.slice(0, limit).map(doc => { + const hasNextPage = results.length > _limit; + const items = results.slice(0, _limit).map(doc => { const { _id, ...rest } = doc; return { ...rest, @@ -232,7 +264,7 @@ export class MongoDBJobsRepository implements IJobsRepository { return { items, - nextCursor: hasNextPage ? results[limit - 1]._id.toString() : null, + nextCursor: hasNextPage ? results[_limit - 1]._id.toString() : null, }; } } diff --git a/apps/rowboat/src/interface-adapters/controllers/jobs/list-jobs.controller.ts b/apps/rowboat/src/interface-adapters/controllers/jobs/list-jobs.controller.ts index af9dcdee..8c089527 100644 --- a/apps/rowboat/src/interface-adapters/controllers/jobs/list-jobs.controller.ts +++ b/apps/rowboat/src/interface-adapters/controllers/jobs/list-jobs.controller.ts @@ -3,13 +3,14 @@ import z from "zod"; import { IListJobsUseCase } from "@/src/application/use-cases/jobs/list-jobs.use-case"; import { Job } from "@/src/entities/models/job"; import { PaginatedList } from "@/src/entities/common/paginated-list"; -import { ListedJobItem } from "@/src/application/repositories/jobs.repository.interface"; +import { JobFiltersSchema, ListedJobItem } from "@/src/application/repositories/jobs.repository.interface"; const inputSchema = z.object({ caller: z.enum(["user", "api"]), userId: z.string().optional(), apiKey: z.string().optional(), projectId: z.string(), + filters: JobFiltersSchema.optional(), cursor: z.string().optional(), limit: z.number().optional(), }); @@ -35,7 +36,7 @@ export class ListJobsController implements IListJobsController { if (!result.success) { throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); } - const { caller, userId, apiKey, projectId, cursor, limit } = result.data; + const { caller, userId, apiKey, projectId, filters, cursor, limit } = result.data; // execute use case return await this.listJobsUseCase.execute({ @@ -43,6 +44,7 @@ export class ListJobsController implements IListJobsController { userId, apiKey, projectId, + filters, cursor, limit, });