Skip to content

Commit 74b25d6

Browse files
Ekaterina BulatovaEkaterina Bulatova
authored andcommitted
perf(webapp): use lightweight loaders for the env var create flow
Opening /environment-variables/new ran the full list presenter twice — once in the parent route loader and once in the child loader — fetching and decrypting every variable value just to show the create form, which only needs the list of environments. - Short-circuit the parent route loader on the /new path so it skips the list presenter entirely and renders only the create outlet. - Load just the environment list in the child route via a new CreateEnvironmentVariablesPresenter. - Extract the shared environment-loading logic into loadEnvironmentVariablesEnvironments, preserving the project access check and environment filtering for both presenters. Removes the heavy presenter work (full fetch + decrypt) when opening the create form.
1 parent 937db1e commit 74b25d6

9 files changed

Lines changed: 702 additions & 40 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Make the Environment Variables page fast for projects with many variables across many environments (windowed SSR + table virtualization, decrypt only non-secret values, lightweight create-page loaders)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { type PrismaClient, prisma } from "~/db.server";
2+
import { type Project } from "~/models/project.server";
3+
import { type User } from "~/models/user.server";
4+
import { loadEnvironmentVariablesEnvironments } from "./environmentVariablesEnvironments.server";
5+
6+
export class CreateEnvironmentVariablesPresenter {
7+
#prismaClient: PrismaClient;
8+
9+
constructor(prismaClient: PrismaClient = prisma) {
10+
this.#prismaClient = prismaClient;
11+
}
12+
13+
public async call({ userId, projectSlug }: { userId: User["id"]; projectSlug: Project["slug"] }) {
14+
return loadEnvironmentVariablesEnvironments(this.#prismaClient, { userId, projectSlug });
15+
}
16+
}

apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { PrismaClient, prisma } from "~/db.server";
22
import { Project } from "~/models/project.server";
33
import { User } from "~/models/user.server";
4-
import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmentSort";
54
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
65
import type { EnvironmentVariableUpdater } from "~/v3/environmentVariables/repository";
76
import {
87
SyncEnvVarsMapping,
98
EnvSlug,
109
} from "~/v3/vercel/vercelProjectIntegrationSchema";
1110
import { VercelIntegrationService } from "~/services/vercelIntegration.server";
11+
import { loadEnvironmentVariablesEnvironments } from "./environmentVariablesEnvironments.server";
1212

1313
type Result = Awaited<ReturnType<EnvironmentVariablesPresenter["call"]>>;
1414
export type EnvironmentVariableWithSetValues = Result["environmentVariables"][number];
@@ -111,29 +111,12 @@ export class EnvironmentVariablesPresenter {
111111
const usersRecord: Record<string, { id: string; name: string | null; displayName: string | null; avatarUrl: string | null }> =
112112
Object.fromEntries(users.map((u) => [u.id, u]));
113113

114-
const environments = await this.#prismaClient.runtimeEnvironment.findMany({
115-
select: {
116-
id: true,
117-
type: true,
118-
isBranchableEnvironment: true,
119-
branchName: true,
120-
orgMember: {
121-
select: {
122-
userId: true,
123-
},
124-
},
125-
},
126-
where: {
127-
project: {
128-
slug: projectSlug,
129-
},
130-
archivedAt: null,
131-
},
132-
});
133-
134-
const sortedEnvironments = sortEnvironments(filterOrphanedEnvironments(environments)).filter(
135-
(e) => e.orgMember?.userId === userId || e.orgMember === null
136-
);
114+
const { environments: sortedEnvironments, hasStaging } =
115+
await loadEnvironmentVariablesEnvironments(
116+
this.#prismaClient,
117+
{ userId, projectSlug },
118+
{ skipProjectAccessCheck: true }
119+
);
137120

138121
const repository = new EnvironmentVariablesRepository(this.#prismaClient);
139122

@@ -215,13 +198,8 @@ export class EnvironmentVariablesPresenter {
215198
});
216199
})
217200
.sort((a, b) => a.key.localeCompare(b.key)),
218-
environments: sortedEnvironments.map((environment) => ({
219-
id: environment.id,
220-
type: environment.type,
221-
isBranchableEnvironment: environment.isBranchableEnvironment,
222-
branchName: environment.branchName,
223-
})),
224-
hasStaging: environments.some((environment) => environment.type === "STAGING"),
201+
environments: sortedEnvironments,
202+
hasStaging,
225203
// Vercel integration data
226204
vercelIntegration: vercelIntegration
227205
? {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
2+
import { type PrismaClient } from "~/db.server";
3+
import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmentSort";
4+
5+
export type EnvironmentVariablesEnvironment = {
6+
id: string;
7+
type: RuntimeEnvironmentType;
8+
isBranchableEnvironment: boolean;
9+
branchName: string | null;
10+
};
11+
12+
export type EnvironmentVariablesEnvironmentsResult = {
13+
environments: EnvironmentVariablesEnvironment[];
14+
hasStaging: boolean;
15+
};
16+
17+
export async function loadEnvironmentVariablesEnvironments(
18+
prismaClient: PrismaClient,
19+
{ userId, projectSlug }: { userId: string; projectSlug: string },
20+
options?: { skipProjectAccessCheck?: boolean }
21+
): Promise<EnvironmentVariablesEnvironmentsResult> {
22+
if (!options?.skipProjectAccessCheck) {
23+
const project = await prismaClient.project.findFirst({
24+
select: {
25+
id: true,
26+
},
27+
where: {
28+
slug: projectSlug,
29+
organization: {
30+
members: {
31+
some: {
32+
userId,
33+
},
34+
},
35+
},
36+
},
37+
});
38+
39+
if (!project) {
40+
throw new Error("Project not found");
41+
}
42+
}
43+
44+
const environments = await prismaClient.runtimeEnvironment.findMany({
45+
select: {
46+
id: true,
47+
type: true,
48+
isBranchableEnvironment: true,
49+
branchName: true,
50+
orgMember: {
51+
select: {
52+
userId: true,
53+
},
54+
},
55+
},
56+
where: {
57+
project: {
58+
slug: projectSlug,
59+
},
60+
archivedAt: null,
61+
},
62+
});
63+
64+
const sortedEnvironments = sortEnvironments(filterOrphanedEnvironments(environments)).filter(
65+
(environment) => environment.orgMember?.userId === userId || environment.orgMember === null
66+
);
67+
68+
return {
69+
environments: sortedEnvironments.map((environment) => ({
70+
id: environment.id,
71+
type: environment.type,
72+
isBranchableEnvironment: environment.isBranchableEnvironment,
73+
branchName: environment.branchName,
74+
})),
75+
hasStaging: environments.some((environment) => environment.type === "STAGING"),
76+
};
77+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { useEnvironment } from "~/hooks/useEnvironment";
3939
import { useList } from "~/hooks/useList";
4040
import { useOrganization } from "~/hooks/useOrganizations";
4141
import { useProject } from "~/hooks/useProject";
42-
import { EnvironmentVariablesPresenter } from "~/presenters/v3/EnvironmentVariablesPresenter.server";
42+
import { CreateEnvironmentVariablesPresenter } from "~/presenters/v3/CreateEnvironmentVariablesPresenter.server";
4343
import { logger } from "~/services/logger.server";
4444
import { requireUserId } from "~/services/session.server";
4545
import { cn } from "~/utils/cn";
@@ -58,7 +58,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
5858
const { projectParam } = ProjectParamSchema.parse(params);
5959

6060
try {
61-
const presenter = new EnvironmentVariablesPresenter();
61+
const presenter = new CreateEnvironmentVariablesPresenter();
6262
const { environments, hasStaging } = await presenter.call({
6363
userId,
6464
projectSlug: projectParam,

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
type EnvironmentVariableWithSetValues,
7070
EnvironmentVariablesPresenter,
7171
} from "~/presenters/v3/EnvironmentVariablesPresenter.server";
72+
import { type EnvironmentVariablesEnvironment } from "~/presenters/v3/environmentVariablesEnvironments.server";
7273
import { requireUserId } from "~/services/session.server";
7374
import { cn } from "~/utils/cn";
7475
import {
@@ -102,11 +103,46 @@ export const meta: MetaFunction = () => {
102103
];
103104
};
104105

106+
type PageVercelIntegration = NonNullable<
107+
Awaited<ReturnType<EnvironmentVariablesPresenter["call"]>>["vercelIntegration"]
108+
>;
109+
110+
type EnvironmentVariablesListLoaderData = {
111+
environmentVariables: EnvironmentVariableWithSetValues[];
112+
environments: EnvironmentVariablesEnvironment[];
113+
hasStaging: boolean;
114+
vercelIntegration: PageVercelIntegration | null;
115+
isCreateRoute: false;
116+
};
117+
118+
type EnvironmentVariablesCreateRouteLoaderData = {
119+
environmentVariables: [];
120+
environments: [];
121+
hasStaging: false;
122+
vercelIntegration: null;
123+
isCreateRoute: true;
124+
};
125+
126+
type EnvironmentVariablesPageLoaderData =
127+
| EnvironmentVariablesListLoaderData
128+
| EnvironmentVariablesCreateRouteLoaderData;
129+
105130
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
106131
const userId = await requireUserId(request);
107132
const { projectParam } = ProjectParamSchema.parse(params);
108133

109134
try {
135+
const url = new URL(request.url);
136+
if (url.pathname.endsWith("/environment-variables/new")) {
137+
return typedjson({
138+
environmentVariables: [],
139+
environments: [],
140+
hasStaging: false,
141+
vercelIntegration: null,
142+
isCreateRoute: true as const,
143+
});
144+
}
145+
110146
const presenter = new EnvironmentVariablesPresenter();
111147
const { environmentVariables, environments, hasStaging, vercelIntegration } =
112148
await presenter.call({
@@ -119,6 +155,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
119155
environments,
120156
hasStaging,
121157
vercelIntegration,
158+
isCreateRoute: false as const,
122159
});
123160
} catch (error) {
124161
console.error(error);
@@ -276,14 +313,39 @@ type GroupedEnvironmentVariable = EnvironmentVariableWithSetValues & {
276313
occurences: number;
277314
};
278315

279-
type PageVercelIntegration = NonNullable<
280-
Awaited<ReturnType<EnvironmentVariablesPresenter["call"]>>["vercelIntegration"]
281-
>;
282-
283316
export default function Page() {
317+
const loaderData = useTypedLoaderData<EnvironmentVariablesPageLoaderData>();
318+
319+
if (loaderData.isCreateRoute) {
320+
return (
321+
<PageContainer>
322+
<NavBar>
323+
<PageTitle title="Environment variables" />
324+
<PageAccessories>
325+
<LinkButton
326+
LeadingIcon={BookOpenIcon}
327+
to={docsPath("v3/deploy-environment-variables")}
328+
variant="docs/small"
329+
>
330+
Environment variables docs
331+
</LinkButton>
332+
</PageAccessories>
333+
</NavBar>
334+
<Outlet />
335+
</PageContainer>
336+
);
337+
}
338+
339+
return <EnvironmentVariablesListPage loaderData={loaderData} />;
340+
}
341+
342+
function EnvironmentVariablesListPage({
343+
loaderData,
344+
}: {
345+
loaderData: EnvironmentVariablesListLoaderData;
346+
}) {
284347
const [revealAll, setRevealAll] = useState(false);
285-
const { environmentVariables, environments, vercelIntegration } =
286-
useTypedLoaderData<typeof loader>();
348+
const { environmentVariables, vercelIntegration } = loaderData;
287349
const organization = useOrganization();
288350
const project = useProject();
289351
const environment = useEnvironment();
@@ -487,7 +549,6 @@ export default function Page() {
487549
</Table>
488550
</div>
489551
</div>
490-
<Outlet />
491552
</PageBody>
492553
</PageContainer>
493554
);

0 commit comments

Comments
 (0)