Skip to content

Commit 6572847

Browse files
committed
perf(webapp): paginate the environment variables settings page
The env-var settings page loaded every variable in the project with a nested values read plus an unused valueReference (SecretReference) sub-load, so a project with many variables pulled variables x environments rows (~18k for large projects) in one burst on each page load. Paginate the presenter by variable key (count + orderBy key + skip/take, page size 50) and drop the never-read valueReference include. This bounds the value read to pageSize x environments per page, removes the SecretReference query entirely, and scopes the secret-value and updater lookups to the current page. Search moves server-side (key, case-insensitive) and the page gains pagination controls. All queries are index-backed: the (projectId, key) unique serves both the count and the ordered pagination (no sort), and the value/secret/user reads use existing indexes with page-scoped IN lists.
1 parent bc3a33b commit 6572847

3 files changed

Lines changed: 70 additions & 24 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+
The environment variables page now loads a page at a time, keeping it fast for projects with a large number of variables. Search matches variable names across every page.

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

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import type { SyncEnvVarsMapping, EnvSlug } from "~/v3/vercel/vercelProjectInteg
88
import { VercelIntegrationService } from "~/services/vercelIntegration.server";
99
import { loadEnvironmentVariablesEnvironments } from "./environmentVariablesEnvironments.server";
1010

11-
import { boundedIn } from "@trigger.dev/database";
11+
import { boundedIn, type Prisma } from "@trigger.dev/database";
1212
type Result = Awaited<ReturnType<EnvironmentVariablesPresenter["call"]>>;
1313
export type EnvironmentVariableWithSetValues = Result["environmentVariables"][number];
1414

15+
export const DEFAULT_ENV_VARS_PAGE_SIZE = 50;
16+
1517
export class EnvironmentVariablesPresenter {
1618
#prismaClient: PrismaClient;
1719
#replicaClient: PrismaReplicaClient;
@@ -21,7 +23,19 @@ export class EnvironmentVariablesPresenter {
2123
this.#replicaClient = replicaClient;
2224
}
2325

24-
public async call({ userId, projectSlug }: { userId: User["id"]; projectSlug: Project["slug"] }) {
26+
public async call({
27+
userId,
28+
projectSlug,
29+
page = 1,
30+
pageSize = DEFAULT_ENV_VARS_PAGE_SIZE,
31+
search,
32+
}: {
33+
userId: User["id"];
34+
projectSlug: Project["slug"];
35+
page?: number;
36+
pageSize?: number;
37+
search?: string;
38+
}) {
2539
const project = await this.#replicaClient.project.findFirst({
2640
select: {
2741
id: true,
@@ -53,6 +67,17 @@ export class EnvironmentVariablesPresenter {
5367
// values in archived branch environments, which would otherwise all be loaded here.
5468
const environmentIds = sortedEnvironments.map((env) => env.id);
5569

70+
const variableWhere: Prisma.EnvironmentVariableWhereInput = {
71+
projectId: project.id,
72+
...(search ? { key: { contains: search, mode: "insensitive" } } : {}),
73+
};
74+
75+
const totalCount = await this.#replicaClient.environmentVariable.count({
76+
where: variableWhere,
77+
});
78+
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
79+
const currentPage = Math.min(Math.max(1, page), totalPages);
80+
5681
const environmentVariables = await this.#replicaClient.environmentVariable.findMany({
5782
select: {
5883
id: true,
@@ -64,11 +89,6 @@ export class EnvironmentVariablesPresenter {
6489
version: true,
6590
lastUpdatedBy: true,
6691
updatedAt: true,
67-
valueReference: {
68-
select: {
69-
key: true,
70-
},
71-
},
7292
isSecret: true,
7393
},
7494
where: {
@@ -78,9 +98,12 @@ export class EnvironmentVariablesPresenter {
7898
},
7999
},
80100
},
81-
where: {
82-
projectId: project.id,
101+
where: variableWhere,
102+
orderBy: {
103+
key: "asc",
83104
},
105+
skip: (currentPage - 1) * pageSize,
106+
take: pageSize,
84107
});
85108

86109
const userIds = new Set(
@@ -204,6 +227,11 @@ export class EnvironmentVariablesPresenter {
204227
.sort((a, b) => a.key.localeCompare(b.key)),
205228
environments: sortedEnvironments,
206229
hasStaging,
230+
pagination: {
231+
currentPage,
232+
totalPages,
233+
totalCount,
234+
},
207235
// Vercel integration data
208236
vercelIntegration: vercelIntegration
209237
? {

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

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { Input } from "~/components/primitives/Input";
4040
import { InputGroup } from "~/components/primitives/InputGroup";
4141
import { Label } from "~/components/primitives/Label";
4242
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
43+
import { PaginationControls } from "~/components/primitives/Pagination";
4344
import { Paragraph } from "~/components/primitives/Paragraph";
4445
import { SearchInput } from "~/components/primitives/SearchInput";
4546
import { Switch } from "~/components/primitives/Switch";
@@ -55,10 +56,8 @@ import {
5556
import { SimpleTooltip } from "~/components/primitives/Tooltip";
5657
import { prisma } from "~/db.server";
5758
import { useEnvironment } from "~/hooks/useEnvironment";
58-
import { useFuzzyFilter } from "~/hooks/useFuzzyFilter";
5959
import { useOrganization } from "~/hooks/useOrganizations";
6060
import { useProject } from "~/hooks/useProject";
61-
import { useSearchParams } from "~/hooks/useSearchParam";
6261
import { redirectWithSuccessMessage } from "~/models/message.server";
6362
import { resolveOrgIdFromSlug } from "~/models/organization.server";
6463
import {
@@ -117,6 +116,8 @@ export type EnvironmentVariablesPageLoaderData = {
117116
accessibleEnvironmentIds: string[];
118117
// Environment ids whose env vars the current role can write (create/edit/delete).
119118
writableEnvironmentIds: string[];
119+
pagination: { currentPage: number; totalPages: number; totalCount: number };
120+
search?: string;
120121
};
121122

122123
export const environmentVariablesRouteId =
@@ -125,22 +126,28 @@ export const environmentVariablesRouteId =
125126
export const loader = dashboardLoader(
126127
{
127128
params: EnvironmentParamSchema,
129+
searchParams: z.object({
130+
page: z.coerce.number().int().min(1).catch(1),
131+
search: z.string().trim().min(1).optional().catch(undefined),
132+
}),
128133
context: async (params) => {
129134
const organizationId = await resolveOrgIdFromSlug(params.organizationSlug);
130135
return organizationId ? { organizationId } : {};
131136
},
132137
// No hard authorization: the page lists every environment. Values in
133138
// environments the role can't read are masked per-tier below.
134139
},
135-
async ({ params, user, ability }) => {
140+
async ({ params, searchParams, user, ability }) => {
136141
const { projectParam } = params;
137142

138143
try {
139144
const presenter = new EnvironmentVariablesPresenter();
140-
const { environmentVariables, environments, hasStaging, vercelIntegration } =
145+
const { environmentVariables, environments, hasStaging, vercelIntegration, pagination } =
141146
await presenter.call({
142147
userId: user.id,
143148
projectSlug: projectParam,
149+
page: searchParams.page,
150+
search: searchParams.search,
144151
});
145152

146153
const accessibleEnvironmentIds = environments
@@ -176,6 +183,8 @@ export const loader = dashboardLoader(
176183
vercelIntegration,
177184
accessibleEnvironmentIds,
178185
writableEnvironmentIds,
186+
pagination,
187+
search: searchParams.search,
179188
});
180189
} catch (error) {
181190
console.error(error);
@@ -392,17 +401,12 @@ function EnvironmentVariablesListPage({
392401
loaderData: EnvironmentVariablesPageLoaderData;
393402
}) {
394403
const [revealAll, setRevealAll] = useState(false);
395-
const { environmentVariables, vercelIntegration } = loaderData;
404+
const { environmentVariables, vercelIntegration, pagination, search } = loaderData;
405+
const hasSearch = Boolean(search);
396406
const organization = useOrganization();
397407
const project = useProject();
398408
const environment = useEnvironment();
399-
const { value } = useSearchParams();
400-
const urlSearch = value("search") ?? "";
401-
const { filteredItems } = useFuzzyFilter<EnvironmentVariableWithSetValues>({
402-
items: environmentVariables,
403-
keys: ["key", "value", "environment.type", "environment.branchName"],
404-
filterText: urlSearch,
405-
});
409+
const filteredItems = environmentVariables;
406410

407411
const tableScrollRef = useRef<HTMLDivElement>(null);
408412

@@ -477,9 +481,9 @@ function EnvironmentVariablesListPage({
477481
</NavBar>
478482
<PageBody scrollable={false}>
479483
<div className={cn("flex h-full min-h-0 flex-col")}>
480-
{environmentVariables.length > 0 && (
484+
{(environmentVariables.length > 0 || hasSearch) && (
481485
<div className="flex items-center justify-between gap-2 px-2 py-2">
482-
<SearchInput placeholder="Search variables…" autoFocus />
486+
<SearchInput placeholder="Search variables…" resetParams={["page"]} autoFocus />
483487
<div className="flex items-center justify-end gap-1.5">
484488
<Switch
485489
variant="secondary/small"
@@ -574,7 +578,7 @@ function EnvironmentVariablesListPage({
574578
<TableBody>
575579
<TableRow>
576580
<TableCell colSpan={vercelColumnCount}>
577-
{environmentVariables.length === 0 ? (
581+
{!hasSearch ? (
578582
<div className="flex flex-col items-center justify-center gap-y-4 py-8">
579583
<Header2>You haven't set any environment variables yet.</Header2>
580584
<LinkButton
@@ -597,6 +601,14 @@ function EnvironmentVariablesListPage({
597601
)}
598602
</Table>
599603
</div>
604+
{pagination.totalPages > 1 && (
605+
<div className="flex items-center justify-end border-t border-grid-dimmed px-2 py-2">
606+
<PaginationControls
607+
currentPage={pagination.currentPage}
608+
totalPages={pagination.totalPages}
609+
/>
610+
</div>
611+
)}
600612
</div>
601613
</PageBody>
602614
<Outlet />

0 commit comments

Comments
 (0)