Skip to content

Set env vars to be "secret" #1923

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4e6c4ea
WIP on secret env vars
matt-aitken Apr 11, 2025
0e8b5d3
Editing individual env var values is working
matt-aitken Apr 12, 2025
7d73552
Sort the env vars by the key
matt-aitken Apr 12, 2025
d930104
Deleting values
matt-aitken Apr 12, 2025
ab60348
Allowing setting secret env vars
matt-aitken Apr 14, 2025
ac16ccc
Added medium switch style
matt-aitken Apr 14, 2025
b224d02
Many style changes to the env var form
matt-aitken Apr 14, 2025
7c57aa6
“Copy text” -> “Copy”
matt-aitken Apr 14, 2025
c7a87da
Draw a divider between hidden buttons
matt-aitken Apr 14, 2025
c1b9426
Env var tweaks
matt-aitken Apr 14, 2025
10e714a
Don’t show Dev:you anymore
matt-aitken Apr 14, 2025
707294e
Grouping the same env var keys together
matt-aitken Apr 16, 2025
2d64f09
Styles improved
matt-aitken Apr 16, 2025
a6223fd
Improved styling of edit panel
matt-aitken Apr 16, 2025
f9adbc2
Fix bun detection, dev flushing, and init command (#1914)
nicktrn Apr 11, 2025
e359aae
Release 4.0.0-v4-beta.1 (#1916)
github-actions[bot] Apr 11, 2025
eded802
Both run engines will only lock to versions they can handle (#1922)
nicktrn Apr 13, 2025
2af4178
Add links to and from deployments (#1921)
nicktrn Apr 14, 2025
ff602fc
Fix current worker deployment getter (#1924)
nicktrn Apr 14, 2025
e1f338d
Add a docs page for the human-in-the-loop example project (#1919)
myftija Apr 14, 2025
02a8713
Managed run controller revamp (#1927)
nicktrn Apr 15, 2025
4efdf64
Release 4.0.0-v4-beta.2 (#1928)
github-actions[bot] Apr 15, 2025
8b7d090
Remove batch ID carryover for non-batch waits (#1930)
nicktrn Apr 16, 2025
f6f628e
Delete project (#1913)
matt-aitken Apr 16, 2025
78f8411
Improve usage flushing (#1931)
nicktrn Apr 16, 2025
6ba85b5
For secret env vars, don’t return the value
matt-aitken Apr 16, 2025
6d508ca
Added a new env var repository function for getting secrets with reda…
matt-aitken Apr 16, 2025
512dbb6
Test task for env vars
matt-aitken Apr 17, 2025
2a45200
Merge remote-tracking branch 'origin/main' into secret-env-vars
matt-aitken Apr 17, 2025
8ace735
Delete heartbeat file, merge mess up
matt-aitken Apr 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/webapp/app/components/primitives/CopyableText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function CopyableText({ value, className }: { value: string; className?:
)}
</span>
}
content={copied ? "Copied!" : "Copy text"}
content={copied ? "Copied!" : "Copy"}
className="font-sans"
disableHoverableContent
/>
Expand Down
7 changes: 7 additions & 0 deletions apps/webapp/app/components/primitives/Switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ const variations = {
"transition group-hover:text-text-bright group-disabled:group-hover:text-text-dimmed"
),
},
medium: {
container:
"flex items-center gap-x-2 rounded-md hover:bg-tertiary py-1.5 px-2 transition focus-custom",
root: "h-4 w-8",
thumb: "size-3.5 data-[state=checked]:translate-x-3.5 data-[state=unchecked]:translate-x-0",
text: "text-sm text-text-dimmed",
},
};

type SwitchProps = React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & {
Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/components/primitives/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,9 @@ export const TableCellMenu = forwardRef<
variants[variant].menuButtonDivider
)}
>
<div className={cn("flex items-center gap-x-0.5")}>{hiddenButtons}</div>
<div className={cn("flex items-center gap-x-0.5 divide-x divide-grid-bright")}>
{hiddenButtons}
</div>
</div>
)}
{/* Always visible buttons */}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { flipCauseOption } from "effect/Cause";
import { PrismaClient, prisma } from "~/db.server";
import { Project } from "~/models/project.server";
import { User } from "~/models/user.server";
Expand Down Expand Up @@ -48,6 +49,7 @@ export class EnvironmentVariablesPresenter {
key: true,
},
},
isSecret: true,
},
},
},
Expand Down Expand Up @@ -82,34 +84,43 @@ export class EnvironmentVariablesPresenter {
},
});

const sortedEnvironments = sortEnvironments(filterOrphanedEnvironments(environments));
const sortedEnvironments = sortEnvironments(filterOrphanedEnvironments(environments)).filter(
(e) => e.orgMember?.userId === userId || e.orgMember === null
);

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

return {
environmentVariables: environmentVariables.map((environmentVariable) => {
const variable = variables.find((v) => v.key === environmentVariable.key);
environmentVariables: environmentVariables
.flatMap((environmentVariable) => {
const variable = variables.find((v) => v.key === environmentVariable.key);

return {
id: environmentVariable.id,
key: environmentVariable.key,
values: sortedEnvironments.reduce((previous, env) => {
return sortedEnvironments.flatMap((env) => {
const val = variable?.values.find((v) => v.environment.id === env.id);
previous[env.id] = {
value: val?.value,
environment: { type: env.type, id: env.id },
};
return { ...previous };
}, {} as Record<string, { value: string | undefined; environment: { type: string; id: string } }>),
};
}),
environments: sortedEnvironments
.filter((e) => e.orgMember?.userId === userId || e.orgMember === null)
.map((environment) => ({
id: environment.id,
type: environment.type,
})),
const isSecret =
environmentVariable.values.find((v) => v.environmentId === env.id)?.isSecret ?? false;

if (!val) {
return [];
}

return [
{
id: environmentVariable.id,
key: environmentVariable.key,
environment: { type: env.type, id: env.id },
value: isSecret ? "" : val.value,
isSecret,
},
];
});
})
.sort((a, b) => a.key.localeCompare(b.key)),
environments: sortedEnvironments.map((environment) => ({
id: environment.id,
type: environment.type,
})),
hasStaging: environments.some((environment) => environment.type === "STAGING"),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { useList } from "~/hooks/useList";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { EnvironmentVariablesPresenter } from "~/presenters/v3/EnvironmentVariablesPresenter.server";
import { logger } from "~/services/logger.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import {
Expand Down Expand Up @@ -87,6 +88,10 @@ const schema = z.object({
if (i === "false") return false;
return;
}, z.boolean()),
isSecret: z.preprocess((i) => {
if (i === "true") return true;
return false;
}, z.boolean()),
environmentIds: z.preprocess((i) => {
if (typeof i === "string") return [i];

Expand Down Expand Up @@ -194,7 +199,7 @@ export default function Page() {
shouldRevalidate: "onSubmit",
});

const [revealAll, setRevealAll] = useState(false);
const [revealAll, setRevealAll] = useState(true);

useEffect(() => {
setIsOpen(true);
Expand All @@ -209,14 +214,10 @@ export default function Page() {
}
}}
>
<DialogContent className="md:max-w-2xl lg:max-w-3xl">
<DialogHeader>New environment variables</DialogHeader>
<Form
method="post"
{...form.props}
className="max-h-[70vh] overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
>
<Fieldset className="mt-2">
<DialogContent className="p-0 pt-2.5 md:max-w-2xl lg:max-w-3xl">
<DialogHeader className="px-4">New environment variables</DialogHeader>
<Form method="post" {...form.props}>
<Fieldset className="max-h-[70vh] overflow-y-auto p-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
<InputGroup fullWidth>
<Label>Environments</Label>
<div className="flex items-center gap-2">
Expand All @@ -237,7 +238,7 @@ export default function Page() {
<TooltipTrigger>
<TextLink
to={v3BillingPath(organization)}
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"
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"
>
<LockClosedIcon className="size-4 text-charcoal-500" />
<EnvironmentLabel environment={{ type: "STAGING" }} className="text-sm" />
Expand All @@ -257,8 +258,21 @@ export default function Page() {
file when running locally.
</Hint>
</InputGroup>
<InputGroup className="w-auto">
<Switch
name="isSecret"
variant="medium"
defaultChecked={false}
value="true"
className="-ml-2 inline-flex w-fit"
label={<span className="text-text-bright">Secret value</span>}
/>
<Hint className="-mt-1">
If enabled, you and your team will not be able to view the values after creation.
</Hint>
</InputGroup>
<InputGroup fullWidth>
<FieldLayout showDeleteButton={false}>
<FieldLayout>
<Label>Keys</Label>
<div className="flex justify-between gap-1">
<Label>Values</Label>
Expand All @@ -280,61 +294,47 @@ export default function Page() {
</InputGroup>

<FormError>{form.error}</FormError>
<FormButtons
confirmButton={
<div className="flex flex-row-reverse items-center gap-2">
<Button
type="submit"
variant="primary/medium"
disabled={isLoading}
name="override"
value="false"
>
{isLoading ? "Saving" : "Save"}
</Button>
<Button
variant="secondary/medium"
disabled={isLoading}
name="override"
value="true"
>
{isLoading ? "Overriding" : "Override"}
</Button>
</div>
}
cancelButton={
<LinkButton
to={v3EnvironmentVariablesPath(organization, project, environment)}
variant="tertiary/medium"
>
Cancel
</LinkButton>
}
/>
</Fieldset>
<FormButtons
className="px-4 pb-4"
confirmButton={
<div className="flex flex-row-reverse items-center gap-2">
<Button
type="submit"
variant="primary/medium"
disabled={isLoading}
name="override"
value="false"
>
{isLoading ? "Saving" : "Save"}
</Button>
<Button
variant="secondary/medium"
disabled={isLoading}
name="override"
value="true"
>
{isLoading ? "Overriding" : "Override"}
</Button>
</div>
}
cancelButton={
<LinkButton
to={v3EnvironmentVariablesPath(organization, project, environment)}
variant="tertiary/medium"
>
Cancel
</LinkButton>
}
/>
</Form>
</DialogContent>
</Dialog>
);
}

function FieldLayout({
children,
showDeleteButton,
}: {
children: React.ReactNode;
showDeleteButton: boolean;
}) {
return (
<div
className={cn(
"grid w-full gap-1.5",
showDeleteButton ? "grid-cols-[1fr_1fr_2rem]" : "grid-cols-[1fr_1fr]"
)}
>
{children}
</div>
);
function FieldLayout({ children }: { children: React.ReactNode }) {
return <div className="grid w-full grid-cols-[1fr_1fr] gap-1.5">{children}</div>;
}

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

return (
<fieldset ref={ref}>
<FieldLayout showDeleteButton={showDeleteButton}>
<FieldLayout>
<Input
id={`${formId}-${baseFieldName}.key`}
name={`${baseFieldName}.key`}
Expand All @@ -467,22 +467,24 @@ function VariableField({
autoFocus={index === 0}
onPaste={onPaste}
/>
<Input
id={`${formId}-${baseFieldName}.value`}
name={`${baseFieldName}.value`}
type={showValue ? "text" : "password"}
placeholder="Not set"
value={value.value}
onChange={(e) => onChange({ ...value, value: e.currentTarget.value })}
/>
{showDeleteButton && (
<Button
variant="minimal/medium"
type="button"
onClick={() => onDelete()}
LeadingIcon={XMarkIcon}
<div className={cn("flex items-center justify-between gap-1")}>
<Input
id={`${formId}-${baseFieldName}.value`}
name={`${baseFieldName}.value`}
type={showValue ? "text" : "password"}
placeholder="Not set"
value={value.value}
onChange={(e) => onChange({ ...value, value: e.currentTarget.value })}
/>
)}
{showDeleteButton && (
<Button
variant="minimal/medium"
type="button"
onClick={() => onDelete()}
LeadingIcon={XMarkIcon}
/>
)}
</div>
</FieldLayout>
<div className="space-y-2">
<FormError id={fields.key.errorId}>{fields.key.error}</FormError>
Expand Down
Loading