mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-03 20:32:39 +02:00
show created jobs under recurring rule
This commit is contained in:
parent
eda3f3821f
commit
c030c4fa83
7 changed files with 130 additions and 23 deletions
|
|
@ -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<IListJobsController>('listJobsController');
|
||||
const fetchJobController = container.resolve<IFetchJobController>('fetchJobController');
|
||||
|
||||
export async function listJobs(request: {
|
||||
projectId: string,
|
||||
filters?: z.infer<typeof JobFiltersSchema>,
|
||||
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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
|||
<div><strong>Rule ID:</strong> <code className="bg-gray-100 dark:bg-gray-700 px-1 rounded">{rule.id}</code></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Jobs Created by This Rule */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">
|
||||
Jobs Created by This Rule
|
||||
</h3>
|
||||
<JobsList
|
||||
projectId={projectId}
|
||||
filters={{ recurringJobRuleId: ruleId }}
|
||||
showTitle={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -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<typeof ListedJobItem>;
|
||||
|
||||
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<ListedItem[]>([]);
|
||||
const [cursor, setCursor] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
|
@ -19,14 +26,22 @@ export function JobsList({ projectId }: { projectId: string }) {
|
|||
const [hasMore, setHasMore] = useState<boolean>(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 (
|
||||
<Panel
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
JOBS
|
||||
showTitle ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{customTitle || "JOBS"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
rightActions={
|
||||
<div className="flex items-center gap-3">
|
||||
{filters && items.length > 0 && (
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{items.length} job{items.length !== 1 ? 's' : ''} found
|
||||
</div>
|
||||
)}
|
||||
{/* Reserved for future actions */}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -132,7 +154,9 @@ export function JobsList({ projectId }: { projectId: string }) {
|
|||
</div>
|
||||
)}
|
||||
{!loading && items.length === 0 && (
|
||||
<p className="mt-4 text-center">No jobs yet.</p>
|
||||
<p className="mt-4 text-center">
|
||||
{filters ? "No jobs found matching the current filters." : "No jobs yet."}
|
||||
</p>
|
||||
)}
|
||||
{!loading && items.length > 0 && (
|
||||
<div className="flex flex-col gap-8">
|
||||
|
|
|
|||
|
|
@ -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<typeof JobFiltersSchema>;
|
||||
|
||||
/**
|
||||
* 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<void>;
|
||||
|
||||
/**
|
||||
* 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<z.infer<ReturnType<typeof PaginatedList<typeof ListedJobItem>>>>;
|
||||
list(
|
||||
projectId: string,
|
||||
filters?: JobFilters,
|
||||
cursor?: string,
|
||||
limit?: number
|
||||
): Promise<z.infer<ReturnType<typeof PaginatedList<typeof ListedJobItem>>>>;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<z.infer<ReturnType<typeof PaginatedList<typeof ListedJobItem>>>> {
|
||||
async list(
|
||||
projectId: string,
|
||||
filters?: JobFilters,
|
||||
cursor?: string,
|
||||
limit: number = 50
|
||||
): Promise<z.infer<ReturnType<typeof PaginatedList<typeof ListedJobItem>>>> {
|
||||
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<z.infer<typeof ListedJobItem> & { _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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue