diff --git a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx index 5e02e772f9..4dd0e7f2ff 100644 --- a/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx +++ b/apps/webapp/app/components/runs/v3/ScheduleFilters.tsx @@ -24,10 +24,6 @@ export const ScheduleListFilters = z.object({ .string() .optional() .transform((value) => (value ? value.split(",") : undefined)), - environments: z - .string() - .optional() - .transform((value) => (value ? value.split(",") : undefined)), type: z.union([z.literal("declarative"), z.literal("imperative")]).optional(), search: z.string().optional(), }); @@ -44,7 +40,7 @@ export function ScheduleFilters({ possibleTasks }: ScheduleFiltersProps) { const navigate = useNavigate(); const location = useOptimisticLocation(); const searchParams = new URLSearchParams(location.search); - const { environments, tasks, page, search, type } = ScheduleListFilters.parse( + const { tasks, page, search, type } = ScheduleListFilters.parse( Object.fromEntries(searchParams.entries()) ); diff --git a/apps/webapp/app/presenters/v3/EditSchedulePresenter.server.ts b/apps/webapp/app/presenters/v3/EditSchedulePresenter.server.ts index caf5b0fc8a..a1d9573c98 100644 --- a/apps/webapp/app/presenters/v3/EditSchedulePresenter.server.ts +++ b/apps/webapp/app/presenters/v3/EditSchedulePresenter.server.ts @@ -1,13 +1,16 @@ -import { RuntimeEnvironmentType } from "@trigger.dev/database"; -import { PrismaClient, prisma } from "~/db.server"; -import { displayableEnvironment } from "~/models/runtimeEnvironment.server"; +import { type RuntimeEnvironmentType } from "@trigger.dev/database"; +import { type PrismaClient, prisma } from "~/db.server"; +import { displayableEnvironment, findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { logger } from "~/services/logger.server"; import { filterOrphanedEnvironments } from "~/utils/environmentSort"; import { getTimezones } from "~/utils/timezones.server"; +import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server"; +import { ServiceValidationError } from "~/v3/services/baseService.server"; type EditScheduleOptions = { userId: string; projectSlug: string; + environmentSlug: string; friendlyId?: string; }; @@ -26,7 +29,7 @@ export class EditSchedulePresenter { this.#prismaClient = prismaClient; } - public async call({ userId, projectSlug, friendlyId }: EditScheduleOptions) { + public async call({ userId, projectSlug, environmentSlug, friendlyId }: EditScheduleOptions) { // Find the project scoped to the organization const project = await this.#prismaClient.project.findFirstOrThrow({ select: { @@ -62,13 +65,25 @@ export class EditSchedulePresenter { }, }); - const possibleTasks = await this.#prismaClient.backgroundWorkerTask.findMany({ - distinct: ["slug"], - where: { - projectId: project.id, - triggerSource: "SCHEDULED", - }, - }); + const environment = await findEnvironmentBySlug(project.id, environmentSlug, userId); + if (!environment) { + throw new ServiceValidationError("No matching environment for project", 404); + } + + //get the latest BackgroundWorker + const latestWorker = await findCurrentWorkerFromEnvironment(environment, this.#prismaClient); + + //get all possible scheduled tasks + const possibleTasks = latestWorker + ? await this.#prismaClient.backgroundWorkerTask.findMany({ + where: { + workerId: latestWorker.id, + projectId: project.id, + runtimeEnvironmentId: environment.id, + triggerSource: "SCHEDULED", + }, + }) + : []; const possibleEnvironments = filterOrphanedEnvironments(project.environments).map( (environment) => { @@ -77,7 +92,7 @@ export class EditSchedulePresenter { ); return { - possibleTasks: possibleTasks.map((task) => task.slug), + possibleTasks: possibleTasks.map((task) => task.slug).sort(), possibleEnvironments, possibleTimezones: getTimezones(), schedule: await this.#getExistingSchedule(friendlyId, possibleEnvironments), diff --git a/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts b/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts index 874eac9c5a..b6df2f9fc4 100644 --- a/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts @@ -6,9 +6,12 @@ import { getLimit } from "~/services/platform.v3.server"; import { CheckScheduleService } from "~/v3/services/checkSchedule.server"; import { calculateNextScheduledTimestamp } from "~/v3/utils/calculateNextSchedule.server"; import { BasePresenter } from "./basePresenter.server"; +import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server"; +import { ServiceValidationError } from "~/v3/services/baseService.server"; type ScheduleListOptions = { projectId: string; + environmentId: string; userId?: string; pageSize?: number; } & ScheduleListFilters; @@ -42,8 +45,8 @@ export class ScheduleListPresenter extends BasePresenter { public async call({ userId, projectId, + environmentId, tasks, - environments, search, page, type, @@ -84,16 +87,45 @@ export class ScheduleListPresenter extends BasePresenter { }, }); + const environment = project.environments.find((env) => env.id === environmentId); + if (!environment) { + throw new ServiceValidationError("No matching environment for project", 404); + } + const schedulesCount = await CheckScheduleService.getUsedSchedulesCount({ prisma: this._replica, environments: project.environments, }); + const limit = await getLimit(project.organizationId, "schedules", 100_000_000); + + //get the latest BackgroundWorker + const latestWorker = await findCurrentWorkerFromEnvironment(environment, this._replica); + if (!latestWorker) { + return { + currentPage: 1, + totalPages: 1, + totalCount: 0, + schedules: [], + possibleTasks: [], + hasFilters, + limits: { + used: schedulesCount, + limit, + }, + filters: { + tasks, + search, + }, + }; + } + //get all possible scheduled tasks const possibleTasks = await this._replica.backgroundWorkerTask.findMany({ - distinct: ["slug"], where: { + workerId: latestWorker.id, projectId: project.id, + runtimeEnvironmentId: environmentId, triggerSource: "SCHEDULED", }, }); @@ -107,7 +139,7 @@ export class ScheduleListPresenter extends BasePresenter { taskIdentifier: tasks ? { in: tasks } : undefined, instances: { some: { - environmentId: environments ? { in: environments } : undefined, + environmentId, }, }, type: filterType, @@ -168,13 +200,11 @@ export class ScheduleListPresenter extends BasePresenter { where: { projectId: project.id, taskIdentifier: tasks ? { in: tasks } : undefined, - instances: environments - ? { - some: { - environmentId: environments ? { in: environments } : undefined, - }, - } - : undefined, + instances: { + some: { + environmentId, + }, + }, type: filterType, AND: search ? { @@ -242,17 +272,12 @@ export class ScheduleListPresenter extends BasePresenter { }; }); - const limit = await getLimit(project.organizationId, "schedules", 100_000_000); - return { currentPage: page, totalPages: Math.ceil(totalCount / pageSize), totalCount: totalCount, schedules, - possibleTasks: possibleTasks.map((task) => task.slug), - possibleEnvironments: project.environments.map((environment) => { - return displayableEnvironment(environment, userId); - }), + possibleTasks: possibleTasks.map((task) => task.slug).sort((a, b) => a.localeCompare(b)), hasFilters, limits: { used: schedulesCount, @@ -260,7 +285,6 @@ export class ScheduleListPresenter extends BasePresenter { }, filters: { tasks, - environments, search, }, }; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam/route.tsx index 16c940df9c..38d330cbb0 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam/route.tsx @@ -15,6 +15,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const result = await presenter.call({ userId, projectSlug: projectParam, + environmentSlug: envParam, friendlyId: scheduleParam, }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx index f91e2c720a..ef69033e7b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx @@ -2,18 +2,19 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { EditSchedulePresenter } from "~/presenters/v3/EditSchedulePresenter.server"; import { requireUserId } from "~/services/session.server"; -import { ProjectParamSchema } from "~/utils/pathBuilder"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; import { humanToCronSupported } from "~/v3/humanToCron.server"; import { UpsertScheduleForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route"; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); - const { projectParam, organizationSlug } = ProjectParamSchema.parse(params); + const { projectParam, envParam, organizationSlug } = EnvironmentParamSchema.parse(params); const presenter = new EditSchedulePresenter(); const result = await presenter.call({ userId, projectSlug: projectParam, + environmentSlug: envParam, }); return typedjson({ ...result, showGenerateField: humanToCronSupported }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx index 49bfe1bcc3..dc420ad657 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx @@ -93,12 +93,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const url = new URL(request.url); const s = Object.fromEntries(url.searchParams.entries()); const filters = ScheduleListFilters.parse(s); - filters.environments = [environment.id]; const presenter = new ScheduleListPresenter(); const list = await presenter.call({ userId, projectId: project.id, + environmentId: environment.id, ...filters, }); @@ -106,15 +106,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }; export default function Page() { - const { - schedules, - possibleTasks, - possibleEnvironments, - hasFilters, - limits, - currentPage, - totalPages, - } = useTypedLoaderData(); + const { schedules, possibleTasks, hasFilters, limits, currentPage, totalPages } = + useTypedLoaderData(); const location = useLocation(); const organization = useOrganization(); const project = useProject(); diff --git a/apps/webapp/app/routes/api.v1.schedules.ts b/apps/webapp/app/routes/api.v1.schedules.ts index 7adbd4f63b..beb232c975 100644 --- a/apps/webapp/app/routes/api.v1.schedules.ts +++ b/apps/webapp/app/routes/api.v1.schedules.ts @@ -100,9 +100,9 @@ export async function loader({ request }: LoaderFunctionArgs) { const result = await presenter.call({ projectId: authenticationResult.environment.projectId, + environmentId: authenticationResult.environment.id, page: params.data.page ?? 1, pageSize: params.data.perPage, - environments: [authenticationResult.environment.id], }); return { diff --git a/internal-packages/database/prisma/migrations/20250429100819_background_worker_index_environment_id_and_created_at/migration.sql b/internal-packages/database/prisma/migrations/20250429100819_background_worker_index_environment_id_and_created_at/migration.sql new file mode 100644 index 0000000000..0f79fa94c9 --- /dev/null +++ b/internal-packages/database/prisma/migrations/20250429100819_background_worker_index_environment_id_and_created_at/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX CONCURRENTLY IF NOT EXISTS "BackgroundWorker_runtimeEnvironmentId_createdAt_idx" ON "BackgroundWorker" ("runtimeEnvironmentId", "createdAt" DESC); \ No newline at end of file diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 80ad979d4b..5f83179943 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -1627,6 +1627,8 @@ model BackgroundWorker { @@unique([projectId, runtimeEnvironmentId, version]) @@index([runtimeEnvironmentId]) + // Get the latest worker for a given environment + @@index([runtimeEnvironmentId, createdAt(sort: Desc)]) } model BackgroundWorkerFile {