Skip to content

Commit 33e7b68

Browse files
matt-aitkennicktrngithub-actions[bot]myftija
authored
Set env vars to be "secret" (#1923)
* WIP on secret env vars * Editing individual env var values is working * Sort the env vars by the key * Deleting values * Allowing setting secret env vars * Added medium switch style * Many style changes to the env var form * “Copy text” -> “Copy” * Draw a divider between hidden buttons * Env var tweaks * Don’t show Dev:you anymore * Grouping the same env var keys together * Styles improved * Improved styling of edit panel * Fix bun detection, dev flushing, and init command (#1914) * update nypm to support text-based bun lockfiles * add nypm changeset * handle dev flushing failures gracefully * fix path normalization for init.ts * add changesets * chore: remove pre.json after exiting pre mode * init command to install v4-beta packages * Revert "chore: remove pre.json after exiting pre mode" This reverts commit f5694fd. * make init default to cli version for all packages * Release 4.0.0-v4-beta.1 (#1916) * chore: Update version for release (v4-beta) * Release 4.0.0-v4-beta.1 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: nicktrn <[email protected]> * Both run engines will only lock to versions they can handle (#1922) * run engine v1 will only lock to v1 deployments * run engine v2 will only lock to managed v2 deployments * test: create background worker and deployment with correct engine version * Add links to and from deployments (#1921) * link from deployments tasks to filtered runs view * jump to deployment * don't add version links for dev (yet) * Fix current worker deployment getter (#1924) * only return last v1 deployment in the shared queue consumer * be explicit about only returning managed deployments * Add a docs page for the human-in-the-loop example project (#1919) * Add a docs page for the human-in-the-loop example project * Order guides, example projects and example tasks alphabetically in the docs list * Managed run controller revamp (#1927) * update nypm to support text-based bun lockfiles * fix retry spans * only download debug logs if admin * add nypm changeset * pull out env override logic * use runner env gather helper * handle dev flushing failures gracefully * fix path normalization for init.ts * add logger * add execution heartbeat service * add snapshot poller service * fix poller * add changesets * create socket in constructor * enable strictPropertyInitialization * deprecate dequeue from version * start is not async * dependency injection in prep for tests * add warm start count to all controller logs * add restore count * pull out run execution logic * temp disable pre * add a controller log when starting an execution * refactor execution and squash some bugs * cleanup completed docker containers by default * execution fixes and logging improvements * don't throw afet abort cleanup * poller should use private interval * rename heartbeat service file * rename HeartbeatService to IntervalService * restore old heartbeat service but deprecate it * use the new interval service everywhere * Revert "temp disable pre" This reverts commit e03f417. * add changeset * replace all run engine find uniques with find first * Release 4.0.0-v4-beta.2 (#1928) * chore: Update version for release (v4-beta) * Release 4.0.0-v4-beta.2 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: nicktrn <[email protected]> * Remove batch ID carryover for non-batch waits (#1930) * add failing test case * do not carry over previous batch id when blocking with waitpoint * delete irrelevant test * Delete project (#1913) * Delete project - Don’t schedule tasks if the project is deleted - Delete queues from the master queues - Add the old delete project UI back in * Mark the project as deleted last * Fix for overriding local variable * Added a todo for deleting env queues * Remove todo * Improve usage flushing (#1931) * add flush to global usage api * enable controller debug logs * initialize usage manager after env overrides * add previous run id to more debug logs * add changeset * For secret env vars, don’t return the value * Added a new env var repository function for getting secrets with redactions * Test task for env vars * Delete heartbeat file, merge mess up --------- Co-authored-by: nicktrn <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Saadi Myftija <[email protected]>
1 parent 1b758bd commit 33e7b68

File tree

16 files changed

+522
-231
lines changed

16 files changed

+522
-231
lines changed

apps/webapp/app/components/primitives/CopyableText.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function CopyableText({ value, className }: { value: string; className?:
5151
)}
5252
</span>
5353
}
54-
content={copied ? "Copied!" : "Copy text"}
54+
content={copied ? "Copied!" : "Copy"}
5555
className="font-sans"
5656
disableHoverableContent
5757
/>

apps/webapp/app/components/primitives/Switch.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ const variations = {
3333
"transition group-hover:text-text-bright group-disabled:group-hover:text-text-dimmed"
3434
),
3535
},
36+
medium: {
37+
container:
38+
"flex items-center gap-x-2 rounded-md hover:bg-tertiary py-1.5 px-2 transition focus-custom",
39+
root: "h-4 w-8",
40+
thumb: "size-3.5 data-[state=checked]:translate-x-3.5 data-[state=unchecked]:translate-x-0",
41+
text: "text-sm text-text-dimmed",
42+
},
3643
};
3744

3845
type SwitchProps = React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & {

apps/webapp/app/components/primitives/Table.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,9 @@ export const TableCellMenu = forwardRef<
349349
variants[variant].menuButtonDivider
350350
)}
351351
>
352-
<div className={cn("flex items-center gap-x-0.5")}>{hiddenButtons}</div>
352+
<div className={cn("flex items-center gap-x-0.5 divide-x divide-grid-bright")}>
353+
{hiddenButtons}
354+
</div>
353355
</div>
354356
)}
355357
{/* Always visible buttons */}

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

+32-21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { flipCauseOption } from "effect/Cause";
12
import { PrismaClient, prisma } from "~/db.server";
23
import { Project } from "~/models/project.server";
34
import { User } from "~/models/user.server";
@@ -48,6 +49,7 @@ export class EnvironmentVariablesPresenter {
4849
key: true,
4950
},
5051
},
52+
isSecret: true,
5153
},
5254
},
5355
},
@@ -82,34 +84,43 @@ export class EnvironmentVariablesPresenter {
8284
},
8385
});
8486

85-
const sortedEnvironments = sortEnvironments(filterOrphanedEnvironments(environments));
87+
const sortedEnvironments = sortEnvironments(filterOrphanedEnvironments(environments)).filter(
88+
(e) => e.orgMember?.userId === userId || e.orgMember === null
89+
);
8690

8791
const repository = new EnvironmentVariablesRepository(this.#prismaClient);
8892
const variables = await repository.getProject(project.id);
8993

9094
return {
91-
environmentVariables: environmentVariables.map((environmentVariable) => {
92-
const variable = variables.find((v) => v.key === environmentVariable.key);
95+
environmentVariables: environmentVariables
96+
.flatMap((environmentVariable) => {
97+
const variable = variables.find((v) => v.key === environmentVariable.key);
9398

94-
return {
95-
id: environmentVariable.id,
96-
key: environmentVariable.key,
97-
values: sortedEnvironments.reduce((previous, env) => {
99+
return sortedEnvironments.flatMap((env) => {
98100
const val = variable?.values.find((v) => v.environment.id === env.id);
99-
previous[env.id] = {
100-
value: val?.value,
101-
environment: { type: env.type, id: env.id },
102-
};
103-
return { ...previous };
104-
}, {} as Record<string, { value: string | undefined; environment: { type: string; id: string } }>),
105-
};
106-
}),
107-
environments: sortedEnvironments
108-
.filter((e) => e.orgMember?.userId === userId || e.orgMember === null)
109-
.map((environment) => ({
110-
id: environment.id,
111-
type: environment.type,
112-
})),
101+
const isSecret =
102+
environmentVariable.values.find((v) => v.environmentId === env.id)?.isSecret ?? false;
103+
104+
if (!val) {
105+
return [];
106+
}
107+
108+
return [
109+
{
110+
id: environmentVariable.id,
111+
key: environmentVariable.key,
112+
environment: { type: env.type, id: env.id },
113+
value: isSecret ? "" : val.value,
114+
isSecret,
115+
},
116+
];
117+
});
118+
})
119+
.sort((a, b) => a.key.localeCompare(b.key)),
120+
environments: sortedEnvironments.map((environment) => ({
121+
id: environment.id,
122+
type: environment.type,
123+
})),
113124
hasStaging: environments.some((environment) => environment.type === "STAGING"),
114125
};
115126
}

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

+79-77
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { useList } from "~/hooks/useList";
4040
import { useOrganization } from "~/hooks/useOrganizations";
4141
import { useProject } from "~/hooks/useProject";
4242
import { EnvironmentVariablesPresenter } from "~/presenters/v3/EnvironmentVariablesPresenter.server";
43+
import { logger } from "~/services/logger.server";
4344
import { requireUserId } from "~/services/session.server";
4445
import { cn } from "~/utils/cn";
4546
import {
@@ -87,6 +88,10 @@ const schema = z.object({
8788
if (i === "false") return false;
8889
return;
8990
}, z.boolean()),
91+
isSecret: z.preprocess((i) => {
92+
if (i === "true") return true;
93+
return false;
94+
}, z.boolean()),
9095
environmentIds: z.preprocess((i) => {
9196
if (typeof i === "string") return [i];
9297

@@ -194,7 +199,7 @@ export default function Page() {
194199
shouldRevalidate: "onSubmit",
195200
});
196201

197-
const [revealAll, setRevealAll] = useState(false);
202+
const [revealAll, setRevealAll] = useState(true);
198203

199204
useEffect(() => {
200205
setIsOpen(true);
@@ -209,14 +214,10 @@ export default function Page() {
209214
}
210215
}}
211216
>
212-
<DialogContent className="md:max-w-2xl lg:max-w-3xl">
213-
<DialogHeader>New environment variables</DialogHeader>
214-
<Form
215-
method="post"
216-
{...form.props}
217-
className="max-h-[70vh] overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
218-
>
219-
<Fieldset className="mt-2">
217+
<DialogContent className="p-0 pt-2.5 md:max-w-2xl lg:max-w-3xl">
218+
<DialogHeader className="px-4">New environment variables</DialogHeader>
219+
<Form method="post" {...form.props}>
220+
<Fieldset className="max-h-[70vh] overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
220221
<InputGroup fullWidth>
221222
<Label>Environments</Label>
222223
<div className="flex items-center gap-2">
@@ -237,7 +238,7 @@ export default function Page() {
237238
<TooltipTrigger>
238239
<TextLink
239240
to={v3BillingPath(organization)}
240-
className="flex w-fit cursor-pointer items-center gap-2 rounded border border-dashed border-charcoal-600 py-3 pl-3 pr-4 transition hover:border-charcoal-500 hover:bg-charcoal-850"
241+
className="flex w-fit cursor-pointer items-center gap-2 rounded border border-dashed border-charcoal-600 py-2.5 pl-3 pr-4 transition hover:border-charcoal-500 hover:bg-charcoal-850"
241242
>
242243
<LockClosedIcon className="size-4 text-charcoal-500" />
243244
<EnvironmentLabel environment={{ type: "STAGING" }} className="text-sm" />
@@ -257,8 +258,21 @@ export default function Page() {
257258
file when running locally.
258259
</Hint>
259260
</InputGroup>
261+
<InputGroup className="w-auto">
262+
<Switch
263+
name="isSecret"
264+
variant="medium"
265+
defaultChecked={false}
266+
value="true"
267+
className="-ml-2 inline-flex w-fit"
268+
label={<span className="text-text-bright">Secret value</span>}
269+
/>
270+
<Hint className="-mt-1">
271+
If enabled, you and your team will not be able to view the values after creation.
272+
</Hint>
273+
</InputGroup>
260274
<InputGroup fullWidth>
261-
<FieldLayout showDeleteButton={false}>
275+
<FieldLayout>
262276
<Label>Keys</Label>
263277
<div className="flex justify-between gap-1">
264278
<Label>Values</Label>
@@ -280,61 +294,47 @@ export default function Page() {
280294
</InputGroup>
281295

282296
<FormError>{form.error}</FormError>
283-
<FormButtons
284-
confirmButton={
285-
<div className="flex flex-row-reverse items-center gap-2">
286-
<Button
287-
type="submit"
288-
variant="primary/medium"
289-
disabled={isLoading}
290-
name="override"
291-
value="false"
292-
>
293-
{isLoading ? "Saving" : "Save"}
294-
</Button>
295-
<Button
296-
variant="secondary/medium"
297-
disabled={isLoading}
298-
name="override"
299-
value="true"
300-
>
301-
{isLoading ? "Overriding" : "Override"}
302-
</Button>
303-
</div>
304-
}
305-
cancelButton={
306-
<LinkButton
307-
to={v3EnvironmentVariablesPath(organization, project, environment)}
308-
variant="tertiary/medium"
309-
>
310-
Cancel
311-
</LinkButton>
312-
}
313-
/>
314297
</Fieldset>
298+
<FormButtons
299+
className="px-4 pb-4"
300+
confirmButton={
301+
<div className="flex flex-row-reverse items-center gap-2">
302+
<Button
303+
type="submit"
304+
variant="primary/medium"
305+
disabled={isLoading}
306+
name="override"
307+
value="false"
308+
>
309+
{isLoading ? "Saving" : "Save"}
310+
</Button>
311+
<Button
312+
variant="secondary/medium"
313+
disabled={isLoading}
314+
name="override"
315+
value="true"
316+
>
317+
{isLoading ? "Overriding" : "Override"}
318+
</Button>
319+
</div>
320+
}
321+
cancelButton={
322+
<LinkButton
323+
to={v3EnvironmentVariablesPath(organization, project, environment)}
324+
variant="tertiary/medium"
325+
>
326+
Cancel
327+
</LinkButton>
328+
}
329+
/>
315330
</Form>
316331
</DialogContent>
317332
</Dialog>
318333
);
319334
}
320335

321-
function FieldLayout({
322-
children,
323-
showDeleteButton,
324-
}: {
325-
children: React.ReactNode;
326-
showDeleteButton: boolean;
327-
}) {
328-
return (
329-
<div
330-
className={cn(
331-
"grid w-full gap-1.5",
332-
showDeleteButton ? "grid-cols-[1fr_1fr_2rem]" : "grid-cols-[1fr_1fr]"
333-
)}
334-
>
335-
{children}
336-
</div>
337-
);
336+
function FieldLayout({ children }: { children: React.ReactNode }) {
337+
return <div className="grid w-full grid-cols-[1fr_1fr] gap-1.5">{children}</div>;
338338
}
339339

340340
function VariableFields({
@@ -409,9 +409,9 @@ function VariableFields({
409409
/>
410410
);
411411
})}
412-
<div className={cn("flex items-center justify-between gap-4", items.length > 1 && "pr-10")}>
412+
<div className="flex items-center justify-between gap-4">
413413
<Paragraph variant="extra-small">
414-
Tip: Paste your .env into this form to populate it.
414+
Tip: Paste all your .env values at once into this form to populate it.
415415
</Paragraph>
416416
<Button
417417
variant="tertiary/medium"
@@ -457,7 +457,7 @@ function VariableField({
457457

458458
return (
459459
<fieldset ref={ref}>
460-
<FieldLayout showDeleteButton={showDeleteButton}>
460+
<FieldLayout>
461461
<Input
462462
id={`${formId}-${baseFieldName}.key`}
463463
name={`${baseFieldName}.key`}
@@ -467,22 +467,24 @@ function VariableField({
467467
autoFocus={index === 0}
468468
onPaste={onPaste}
469469
/>
470-
<Input
471-
id={`${formId}-${baseFieldName}.value`}
472-
name={`${baseFieldName}.value`}
473-
type={showValue ? "text" : "password"}
474-
placeholder="Not set"
475-
value={value.value}
476-
onChange={(e) => onChange({ ...value, value: e.currentTarget.value })}
477-
/>
478-
{showDeleteButton && (
479-
<Button
480-
variant="minimal/medium"
481-
type="button"
482-
onClick={() => onDelete()}
483-
LeadingIcon={XMarkIcon}
470+
<div className={cn("flex items-center justify-between gap-1")}>
471+
<Input
472+
id={`${formId}-${baseFieldName}.value`}
473+
name={`${baseFieldName}.value`}
474+
type={showValue ? "text" : "password"}
475+
placeholder="Not set"
476+
value={value.value}
477+
onChange={(e) => onChange({ ...value, value: e.currentTarget.value })}
484478
/>
485-
)}
479+
{showDeleteButton && (
480+
<Button
481+
variant="minimal/medium"
482+
type="button"
483+
onClick={() => onDelete()}
484+
LeadingIcon={XMarkIcon}
485+
/>
486+
)}
487+
</div>
486488
</FieldLayout>
487489
<div className="space-y-2">
488490
<FormError id={fields.key.errorId}>{fields.key.error}</FormError>

0 commit comments

Comments
 (0)