Skip to content

Commit 4bcd654

Browse files
authored
Merge pull request #185 from SSHari/patch-573
feat: 🎸 move get project jobs to lower level in app hierarchy
2 parents 2a6cd98 + ca59b6b commit 4bcd654

13 files changed

Lines changed: 288 additions & 206 deletions

File tree

apps/webapp/app/components/jobs/JobsTable.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
} from "../primitives/Table";
1616
import { SimpleTooltip } from "../primitives/Tooltip";
1717
import { runStatusTitle } from "../runs/RunStatuses";
18-
import { ProjectJob, useProject } from "~/hooks/useProject";
18+
import { ProjectJob } from "~/hooks/useJobs";
19+
import { useProject } from "~/hooks/useProject";
1920
import { useOrganization } from "~/hooks/useOrganizations";
2021
import { JobRunStatus } from "~/models/job.server";
2122
import { cn } from "~/utils/cn";

apps/webapp/app/components/navigation/JobsMenu.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useState } from "react";
33
import { useJob } from "~/hooks/useJob";
44
import { useOrganization } from "~/hooks/useOrganizations";
55
import { useProject } from "~/hooks/useProject";
6+
import { useJobs } from "~/hooks/useJobs";
67
import { cn } from "~/utils/cn";
78
import { jobPath } from "~/utils/pathBuilder";
89
import { LabelValueStack } from "../primitives/LabelValueStack";
@@ -18,6 +19,7 @@ export function JobsMenu({ matches }: { matches: RouteMatch[] }) {
1819
const [isOpen, setIsOpen] = useState(false);
1920
const organization = useOrganization(matches);
2021
const project = useProject(matches);
22+
const projectJobs = useJobs(matches);
2123
const currentJob = useJob(matches);
2224

2325
return (
@@ -33,7 +35,7 @@ export function JobsMenu({ matches }: { matches: RouteMatch[] }) {
3335
>
3436
<PopoverSectionHeader title="Jobs" />
3537
<div className="flex flex-col gap-1 p-1">
36-
{project.jobs.map((job) => {
38+
{projectJobs.map((job) => {
3739
const isSelected = job.id === currentJob?.id;
3840
return (
3941
<Link

apps/webapp/app/hooks/useFilterJobs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ProjectJob } from "./useProject";
1+
import { ProjectJob } from "./useJobs";
22
import { useTextFilter } from "./useTextFilter";
33

44
export function useFilterJobs(jobs: ProjectJob[]) {

apps/webapp/app/hooks/useJob.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import {
2-
UseDataFunctionReturn,
3-
useTypedRouteLoaderData,
4-
} from "remix-typedjson";
1+
import { UseDataFunctionReturn } from "remix-typedjson";
52
import invariant from "tiny-invariant";
63
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
7-
import { useOptionalProject } from "./useProject";
84
import { useChanged } from "./useChanged";
95
import { RouteMatch } from "@remix-run/react";
106
import { useTypedMatchesData } from "./useTypedMatchData";
@@ -14,18 +10,16 @@ export type MatchedJob = UseDataFunctionReturn<typeof loader>["job"];
1410
export const jobMatchId =
1511
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam";
1612
export function useOptionalJob(matches?: RouteMatch[]) {
17-
const project = useOptionalProject(matches);
1813
const routeMatch = useTypedMatchesData<typeof loader>({
1914
id: jobMatchId,
2015
matches,
2116
});
2217

23-
if (!project || !routeMatch || !routeMatch.job) {
18+
if (!routeMatch || !routeMatch.job) {
2419
return undefined;
2520
}
2621

27-
//get the job from the list on the project
28-
return project.jobs.find((j) => j.id === routeMatch.job.id);
22+
return routeMatch.projectJobs.find((j) => j.id === routeMatch.job.id);
2923
}
3024

3125
export function useJob(matches?: RouteMatch[]) {

apps/webapp/app/hooks/useJobs.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { UseDataFunctionReturn } from "remix-typedjson";
2+
import invariant from "tiny-invariant";
3+
import type { loader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam/route";
4+
import { RouteMatch } from "@remix-run/react";
5+
import { useTypedMatchesData } from "./useTypedMatchData";
6+
7+
export type ProjectJob = UseDataFunctionReturn<
8+
typeof loader
9+
>["projectJobs"][number];
10+
11+
export const jobsMatchId =
12+
"routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam";
13+
export function useOptionalJobs(matches?: RouteMatch[]) {
14+
const routeMatch = useTypedMatchesData<typeof loader>({
15+
id: jobsMatchId,
16+
matches,
17+
});
18+
19+
return routeMatch?.projectJobs;
20+
}
21+
22+
export function useJobs(matches?: RouteMatch[]) {
23+
const jobs = useOptionalJobs(matches);
24+
invariant(jobs, "Jobs must be defined");
25+
return jobs;
26+
}

apps/webapp/app/hooks/useProject.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { useChanged } from "./useChanged";
66
import { useTypedMatchesData } from "./useTypedMatchData";
77

88
export type MatchedProject = UseDataFunctionReturn<typeof loader>["project"];
9-
export type ProjectJob = MatchedProject["jobs"][number];
109

1110
export const projectMatchId =
1211
"routes/_app.orgs.$organizationSlug.projects.$projectParam";

apps/webapp/app/presenters/IntegrationClientJobsPresenter.server.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import {
2+
DisplayProperty,
3+
DisplayPropertySchema,
4+
EventSpecificationSchema,
5+
} from "@trigger.dev/internal";
6+
import { PrismaClient, Prisma, prisma } from "~/db.server";
7+
import { Organization } from "~/models/organization.server";
8+
import { Project } from "~/models/project.server";
9+
import { User } from "~/models/user.server";
10+
import { z } from "zod";
11+
12+
export class JobListPresenter {
13+
#prismaClient: PrismaClient;
14+
15+
constructor(prismaClient: PrismaClient = prisma) {
16+
this.#prismaClient = prismaClient;
17+
}
18+
19+
public async call({
20+
userId,
21+
projectSlug,
22+
organizationSlug,
23+
integrationSlug,
24+
}: {
25+
userId: User["id"];
26+
projectSlug: Project["slug"];
27+
organizationSlug?: Organization["slug"];
28+
integrationSlug?: string;
29+
}) {
30+
const orgWhere: Prisma.JobWhereInput["organization"] = organizationSlug
31+
? { slug: organizationSlug, members: { some: { userId } } }
32+
: { members: { some: { userId } } };
33+
34+
const integrationsWhere: Prisma.JobWhereInput["integrations"] =
35+
integrationSlug
36+
? { some: { integration: { slug: integrationSlug } } }
37+
: {};
38+
39+
const jobs = await this.#prismaClient.job.findMany({
40+
select: {
41+
id: true,
42+
slug: true,
43+
title: true,
44+
aliases: {
45+
select: {
46+
version: {
47+
select: {
48+
version: true,
49+
eventSpecification: true,
50+
properties: true,
51+
runs: {
52+
select: {
53+
createdAt: true,
54+
status: true,
55+
},
56+
take: 1,
57+
orderBy: [{ createdAt: "desc" }],
58+
},
59+
integrations: {
60+
select: {
61+
key: true,
62+
integration: {
63+
select: {
64+
slug: true,
65+
definition: true,
66+
setupStatus: true,
67+
},
68+
},
69+
},
70+
},
71+
},
72+
},
73+
environment: {
74+
select: {
75+
type: true,
76+
orgMember: {
77+
select: {
78+
userId: true,
79+
},
80+
},
81+
},
82+
},
83+
},
84+
where: {
85+
name: "latest",
86+
},
87+
},
88+
dynamicTriggers: {
89+
select: {
90+
type: true,
91+
},
92+
},
93+
},
94+
where: {
95+
internal: false,
96+
organization: orgWhere,
97+
project: {
98+
slug: projectSlug,
99+
},
100+
integrations: integrationsWhere,
101+
},
102+
orderBy: [{ title: "asc" }],
103+
});
104+
105+
return jobs
106+
.map((job) => {
107+
//the best alias to select:
108+
// 1. Logged-in user dev
109+
// 2. Prod
110+
// 3. Any other user's dev
111+
const sortedAliases = job.aliases.sort((a, b) => {
112+
if (
113+
a.environment.type === "DEVELOPMENT" &&
114+
a.environment.orgMember?.userId === userId
115+
) {
116+
return -1;
117+
}
118+
119+
if (
120+
b.environment.type === "DEVELOPMENT" &&
121+
b.environment.orgMember?.userId === userId
122+
) {
123+
return 1;
124+
}
125+
126+
if (a.environment.type === "PRODUCTION") {
127+
return -1;
128+
}
129+
130+
if (b.environment.type === "PRODUCTION") {
131+
return 1;
132+
}
133+
134+
return 0;
135+
});
136+
137+
const alias = sortedAliases.at(0);
138+
139+
if (!alias) {
140+
throw new Error(
141+
`No aliases found for job ${job.id}, this should never happen.`
142+
);
143+
}
144+
145+
const eventSpecification = EventSpecificationSchema.parse(
146+
alias.version.eventSpecification
147+
);
148+
149+
const lastRun =
150+
alias.version.runs[0] != null ? alias.version.runs[0] : undefined;
151+
152+
const integrations = alias.version.integrations.map((integration) => ({
153+
key: integration.key,
154+
title: integration.integration.slug,
155+
icon: integration.integration.definition.id,
156+
setupStatus: integration.integration.setupStatus,
157+
}));
158+
159+
let properties: DisplayProperty[] = [];
160+
161+
if (eventSpecification.properties) {
162+
properties = [...properties, ...eventSpecification.properties];
163+
}
164+
165+
if (alias.version.properties) {
166+
const versionProperties = z
167+
.array(DisplayPropertySchema)
168+
.parse(alias.version.properties);
169+
properties = [...properties, ...versionProperties];
170+
}
171+
172+
return {
173+
id: job.id,
174+
slug: job.slug,
175+
title: job.title,
176+
version: alias.version.version,
177+
dynamic: job.dynamicTriggers.length > 0,
178+
event: {
179+
title: eventSpecification.title,
180+
icon: eventSpecification.icon,
181+
source: eventSpecification.source,
182+
},
183+
integrations,
184+
hasIntegrationsRequiringAction: integrations.some(
185+
(i) => i.setupStatus === "MISSING_FIELDS"
186+
),
187+
lastRun,
188+
properties,
189+
};
190+
})
191+
.filter(Boolean);
192+
}
193+
}

0 commit comments

Comments
 (0)