Skip to content

Commit 3b92fbd

Browse files
committed
feat(env-vars): add updater metadata and include user info
Add a discriminated EnvironmentVariableUpdater schema to represent who last updated a variable (user or integration) and thread it through create/edit/value types. Persist lastUpdatedBy and version fields in presenter queries, collect referenced user records, and resolve a display-friendly updatedBy user object (id, name, avatar) for each environment variable value. Also ensure isSecret is read from the value record and avoid returning secrets' values. These changes allow auditing of who modified env vars (user vs integration), enable showing updater details in the UI, and make the presenter robust against missing value records.
1 parent 425ebe3 commit 3b92fbd

File tree

8 files changed

+231
-75
lines changed

8 files changed

+231
-75
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function VercelLogo({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
viewBox="0 0 76 65"
5+
fill="none"
6+
xmlns="http://www.w3.org/2000/svg"
7+
className={className}
8+
>
9+
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="currentColor" />
10+
</svg>
11+
);
12+
}

apps/webapp/app/models/vercelIntegration.server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,6 +1395,10 @@ export class VercelIntegrationRepository {
13951395
key: v.key,
13961396
value: v.value,
13971397
})),
1398+
lastUpdatedBy: {
1399+
type: "integration",
1400+
integration: "vercel",
1401+
},
13981402
});
13991403

14001404
if (result.success) {
@@ -1423,6 +1427,10 @@ export class VercelIntegrationRepository {
14231427
key: v.key,
14241428
value: v.value,
14251429
})),
1430+
lastUpdatedBy: {
1431+
type: "integration",
1432+
integration: "vercel",
1433+
},
14261434
});
14271435

14281436
if (result.success) {

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

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Project } from "~/models/project.server";
33
import { User } from "~/models/user.server";
44
import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmentSort";
55
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
6+
import type { EnvironmentVariableUpdater } from "~/v3/environmentVariables/repository";
67
import {
78
SyncEnvVarsMapping,
89
EnvSlug,
@@ -48,6 +49,9 @@ export class EnvironmentVariablesPresenter {
4849
select: {
4950
id: true,
5051
environmentId: true,
52+
version: true,
53+
lastUpdatedBy: true,
54+
updatedAt: true,
5155
valueReference: {
5256
select: {
5357
key: true,
@@ -71,6 +75,42 @@ export class EnvironmentVariablesPresenter {
7175
},
7276
});
7377

78+
const userIds = new Set(
79+
environmentVariables
80+
.flatMap((envVar) => envVar.values)
81+
.map((value) => value.lastUpdatedBy)
82+
.filter(
83+
(lastUpdatedBy): lastUpdatedBy is { type: "user"; userId: string } =>
84+
lastUpdatedBy !== null &&
85+
typeof lastUpdatedBy === "object" &&
86+
"type" in lastUpdatedBy &&
87+
lastUpdatedBy.type === "user" &&
88+
"userId" in lastUpdatedBy &&
89+
typeof lastUpdatedBy.userId === "string"
90+
)
91+
.map((lastUpdatedBy) => lastUpdatedBy.userId)
92+
);
93+
94+
const users =
95+
userIds.size > 0
96+
? await this.#prismaClient.user.findMany({
97+
where: {
98+
id: {
99+
in: Array.from(userIds),
100+
},
101+
},
102+
select: {
103+
id: true,
104+
name: true,
105+
displayName: true,
106+
avatarUrl: true,
107+
},
108+
})
109+
: [];
110+
111+
const usersRecord: Record<string, { id: string; name: string | null; displayName: string | null; avatarUrl: string | null }> =
112+
Object.fromEntries(users.map((u) => [u.id, u]));
113+
74114
const environments = await this.#prismaClient.runtimeEnvironment.findMany({
75115
select: {
76116
id: true,
@@ -117,20 +157,40 @@ export class EnvironmentVariablesPresenter {
117157

118158
return sortedEnvironments.flatMap((env) => {
119159
const val = variable?.values.find((v) => v.environment.id === env.id);
120-
const isSecret =
121-
environmentVariable.values.find((v) => v.environmentId === env.id)?.isSecret ?? false;
160+
const valueRecord = environmentVariable.values.find((v) => v.environmentId === env.id);
161+
const isSecret = valueRecord?.isSecret ?? false;
122162

123-
if (!val) {
163+
if (!val || !valueRecord) {
124164
return [];
125165
}
126166

167+
const lastUpdatedBy = valueRecord.lastUpdatedBy as EnvironmentVariableUpdater | null;
168+
169+
const updatedByUser =
170+
lastUpdatedBy?.type === "user"
171+
? (() => {
172+
const user = usersRecord[lastUpdatedBy.userId];
173+
return user
174+
? {
175+
id: user.id,
176+
name: user.displayName || user.name || "Unknown",
177+
avatarUrl: user.avatarUrl,
178+
}
179+
: null;
180+
})()
181+
: null;
182+
127183
return [
128184
{
129185
id: environmentVariable.id,
130186
key: environmentVariable.key,
131187
environment: { type: env.type, id: env.id, branchName: env.branchName },
132188
value: isSecret ? "" : val.value,
133189
isSecret,
190+
version: valueRecord.version,
191+
lastUpdatedBy,
192+
updatedByUser,
193+
updatedAt: valueRecord.updatedAt,
134194
},
135195
];
136196
});

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
151151
}
152152

153153
const repository = new EnvironmentVariablesRepository(prisma);
154-
const result = await repository.create(project.id, submission.value);
154+
const result = await repository.create(project.id, {
155+
...submission.value,
156+
lastUpdatedBy: {
157+
type: "user",
158+
userId,
159+
},
160+
});
155161

156162
if (!result.success) {
157163
if (result.variableErrors) {

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

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ import { useEffect, useMemo, useState } from "react";
1919
import { typedjson, useTypedLoaderData } from "remix-typedjson";
2020
import { z } from "zod";
2121
import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel";
22+
import { VercelLogo } from "~/components/integrations/VercelLogo";
2223
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
2324
import { Button, LinkButton } from "~/components/primitives/Buttons";
2425
import { ClipboardField } from "~/components/primitives/ClipboardField";
2526
import { CopyableText } from "~/components/primitives/CopyableText";
27+
import { DateTime } from "~/components/primitives/DateTime";
2628
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
2729
import { Fieldset } from "~/components/primitives/Fieldset";
2830
import { FormButtons } from "~/components/primitives/FormButtons";
@@ -70,6 +72,7 @@ import {
7072
EditEnvironmentVariableValue,
7173
EnvironmentVariable,
7274
} from "~/v3/environmentVariables/repository";
75+
import { UserAvatar } from "~/components/UserProfilePhoto";
7376
import { VercelIntegrationService } from "~/services/vercelIntegration.server";
7477
import { shouldSyncEnvVar, isPullEnvVarsEnabledForEnvironment, type TriggerEnvironmentType } from "~/v3/vercel/vercelProjectIntegrationSchema";
7578

@@ -160,7 +163,13 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
160163
switch (submission.value.action) {
161164
case "edit": {
162165
const repository = new EnvironmentVariablesRepository(prisma);
163-
const result = await repository.editValue(project.id, submission.value);
166+
const result = await repository.editValue(project.id, {
167+
...submission.value,
168+
lastUpdatedBy: {
169+
type: "user",
170+
userId,
171+
},
172+
});
164173

165174
if (!result.success) {
166175
submission.error.key = [result.error];
@@ -307,17 +316,17 @@ export default function Page() {
307316
<Table containerClassName={cn(filteredItems.length === 0 && "border-t-0")}>
308317
<TableHeader>
309318
<TableRow>
310-
<TableHeaderCell className={vercelIntegration?.enabled ? "w-[22%]" : "w-[25%]"}>
319+
<TableHeaderCell className={vercelIntegration?.enabled ? "w-[21%]" : "w-[24%]"}>
311320
Key
312321
</TableHeaderCell>
313-
<TableHeaderCell className={vercelIntegration?.enabled ? "w-[45%]" : "w-[55%]"}>
322+
<TableHeaderCell className={vercelIntegration?.enabled ? "w-[30%]" : "w-[35%]"}>
314323
Value
315324
</TableHeaderCell>
316-
<TableHeaderCell className={vercelIntegration?.enabled ? "w-[18%]" : "w-[20%]"}>
325+
<TableHeaderCell className={vercelIntegration?.enabled ? "w-[12%]" : "w-[15%]"}>
317326
Environment
318327
</TableHeaderCell>
319328
{vercelIntegration?.enabled && (
320-
<TableHeaderCell className="w-[10%]">
329+
<TableHeaderCell className="w-[8%]">
321330
<SimpleTooltip
322331
button={
323332
<span className="flex items-center gap-1">
@@ -329,7 +338,10 @@ export default function Page() {
329338
/>
330339
</TableHeaderCell>
331340
)}
332-
<TableHeaderCell hiddenLabel className="pl-24">
341+
<TableHeaderCell className="w-[3%]">Ver</TableHeaderCell>
342+
<TableHeaderCell className="w-[15%]">Updated by</TableHeaderCell>
343+
<TableHeaderCell className="w-[14%]">Updated at</TableHeaderCell>
344+
<TableHeaderCell hiddenLabel className="w-[1%] min-w-[3rem]">
333345
Actions
334346
</TableHeaderCell>
335347
</TableRow>
@@ -407,8 +419,36 @@ export default function Page() {
407419
)}
408420
</TableCell>
409421
)}
422+
<TableCell className={cn(cellClassName, borderedCellClassName)}>
423+
<span className="text-sm text-text-dimmed">{variable.version}</span>
424+
</TableCell>
425+
<TableCell className={cn(cellClassName, borderedCellClassName)}>
426+
{variable.updatedByUser ? (
427+
<div className="flex items-center gap-2">
428+
<UserAvatar
429+
avatarUrl={variable.updatedByUser.avatarUrl}
430+
name={variable.updatedByUser.name}
431+
className="size-5"
432+
/>
433+
<span className="text-sm">{variable.updatedByUser.name}</span>
434+
</div>
435+
) : variable.lastUpdatedBy?.type === "integration" ? (
436+
<div className="flex items-center gap-2">
437+
<VercelLogo className="size-4 text-text-dimmed group-hover/table-row:text-text-bright transition-colors" />
438+
<span className="text-sm text-text-dimmed group-hover/table-row:text-text-bright capitalize transition-colors">
439+
{variable.lastUpdatedBy.integration}
440+
</span>
441+
</div>
442+
) : null}
443+
</TableCell>
444+
<TableCell className={cn(cellClassName, borderedCellClassName)}>
445+
{variable.updatedAt ? (
446+
<DateTime date={variable.updatedAt} includeSeconds={false} />
447+
) : null}
448+
</TableCell>
410449
<TableCellMenu
411450
isSticky
451+
className="w-[1%] min-w-[3rem] [&:has(.group-hover/table-row:block)]:w-auto"
412452
hiddenButtons={
413453
<>
414454
<EditEnvironmentVariablePanel
@@ -424,7 +464,7 @@ export default function Page() {
424464
})
425465
) : (
426466
<TableRow>
427-
<TableCell colSpan={4}>
467+
<TableCell colSpan={vercelIntegration?.enabled ? 8 : 7}>
428468
{environmentVariables.length === 0 ? (
429469
<div className="flex flex-col items-center justify-center gap-y-4 py-8">
430470
<Header2>You haven't set any environment variables yet.</Header2>

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
TooltipTrigger,
4545
TooltipProvider
4646
} from "~/components/primitives/Tooltip";
47+
import { VercelLogo } from "~/components/integrations/VercelLogo";
4748
import {
4849
EnvironmentIcon,
4950
environmentFullTitle,
@@ -483,18 +484,6 @@ export function vercelResourcePath(
483484
return `/resources/orgs/${organizationSlug}/projects/${projectSlug}/env/${environmentSlug}/vercel`;
484485
}
485486

486-
function VercelIcon({ className }: { className?: string }) {
487-
return (
488-
<svg
489-
className={className}
490-
viewBox="0 0 76 65"
491-
fill="currentColor"
492-
xmlns="http://www.w3.org/2000/svg"
493-
>
494-
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" />
495-
</svg>
496-
);
497-
}
498487

499488
function VercelConnectionPrompt({
500489
organizationSlug,
@@ -538,7 +527,7 @@ function VercelConnectionPrompt({
538527
LeadingIcon={
539528
isLoadingProjects
540529
? () => <SpinnerWhite className="size-4" />
541-
: () => <VercelIcon className="size-4 -mx-1" />
530+
: () => <VercelLogo className="size-4 -mx-1" />
542531
}
543532
>
544533
{isLoadingProjects ? "Loading projects..." : "Connect Vercel project"}
@@ -557,7 +546,7 @@ function VercelConnectionPrompt({
557546
<LinkButton
558547
to={installPath}
559548
variant="secondary/medium"
560-
LeadingIcon={() => <VercelIcon className="size-4 -mx-1" />}
549+
LeadingIcon={() => <VercelLogo className="size-4 -mx-1" />}
561550
>
562551
Install Vercel app
563552
</LinkButton>
@@ -708,7 +697,7 @@ function ConnectedVercelProjectForm({
708697
{/* Connected project info */}
709698
<div className="mb-4 flex items-center justify-between rounded-sm border bg-grid-dimmed p-2">
710699
<div className="flex items-center gap-2">
711-
<VercelIcon className="size-4" />
700+
<VercelLogo className="size-4" />
712701
<span className="max-w-52 truncate text-sm text-text-bright">
713702
{connectedProject.vercelProjectName}
714703
</span>
@@ -1535,7 +1524,7 @@ function VercelOnboardingModal({
15351524
<DialogContent className="max-w-lg">
15361525
<DialogHeader>
15371526
<div className="flex items-center gap-2">
1538-
<VercelIcon className="size-5" />
1527+
<VercelLogo className="size-5" />
15391528
<span>Set up Vercel Integration</span>
15401529
</div>
15411530
</DialogHeader>
@@ -1558,7 +1547,7 @@ function VercelOnboardingModal({
15581547
<DialogContent className="max-w-lg">
15591548
<DialogHeader>
15601549
<div className="flex items-center gap-2">
1561-
<VercelIcon className="size-5" />
1550+
<VercelLogo className="size-5" />
15621551
<span>Set up Vercel Integration</span>
15631552
</div>
15641553
</DialogHeader>

0 commit comments

Comments
 (0)