feat: renamed old frontend and fixed some minor bugs

This commit is contained in:
=
2025-01-06 14:56:49 +05:30
parent 8c902d7699
commit 54377a44d3
2577 changed files with 35240 additions and 35116 deletions
@@ -0,0 +1,260 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faClone, faFileImport, faSquareCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
FilterableSelect,
FormControl,
IconButton,
Modal,
ModalContent,
ModalTrigger,
Switch,
Tooltip
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api";
const formSchema = z.object({
environment: z.object({ name: z.string(), slug: z.string() }),
secretPath: z
.string()
.trim()
.transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
),
secrets: z
.object({ key: z.string(), value: z.string().optional() })
.array()
.min(1, "Select one or more secrets to copy")
});
type TFormSchema = z.infer<typeof formSchema>;
type Props = {
isOpen?: boolean;
isSmaller?: boolean;
onToggle: (isOpen: boolean) => void;
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
environments?: { name: string; slug: string }[];
workspaceId: string;
environment: string;
secretPath: string;
};
export const CopySecretsFromBoard = ({
environments = [],
workspaceId,
environment,
secretPath,
isOpen,
isSmaller,
onToggle,
onParsedEnv
}: Props) => {
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
const {
handleSubmit,
control,
watch,
reset,
setValue,
formState: { isDirty }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
defaultValues: { secretPath: "/", environment: environments?.[0] }
});
const envCopySecPath = watch("secretPath");
const selectedEnvSlug = watch("environment");
const [debouncedEnvCopySecretPath] = useDebounce(envCopySecPath);
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
environment: selectedEnvSlug.slug,
secretPath: debouncedEnvCopySecretPath,
options: {
enabled:
Boolean(workspaceId) &&
Boolean(selectedEnvSlug) &&
Boolean(debouncedEnvCopySecretPath) &&
isOpen
}
});
useEffect(() => {
setValue("secrets", []);
}, [debouncedEnvCopySecretPath, selectedEnvSlug]);
const handleSecSelectAll = () => {
if (secrets) {
setValue("secrets", secrets, { shouldDirty: true });
}
};
const handleFormSubmit = async (data: TFormSchema) => {
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
data.secrets.forEach(({ key, value }) => {
secretsToBePulled[key] = {
value: (shouldIncludeValues && value) || "",
comments: [""]
};
});
onParsedEnv(secretsToBePulled);
onToggle(false);
reset();
};
return (
<Modal
isOpen={isOpen}
onOpenChange={(state) => {
onToggle(state);
reset();
}}
>
<ModalTrigger asChild>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
onClick={() => onToggle(true)}
isDisabled={!isAllowed}
variant="star"
size={isSmaller ? "xs" : "sm"}
>
Copy Secrets From An Environment
</Button>
)}
</ProjectPermissionCan>
</div>
</ModalTrigger>
<ModalContent
bodyClassName="overflow-visible"
className="max-w-2xl"
title="Copy Secret From An Environment"
subTitle="Copy/paste secrets from other environments into this context"
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="flex items-center space-x-2">
<Controller
control={control}
name="environment"
render={({ field: { value, onChange } }) => (
<FormControl label="Environment" isRequired className="w-1/3">
<FilterableSelect
value={value}
onChange={onChange}
options={environments}
placeholder="Select environment..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.slug}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
render={({ field }) => (
<FormControl label="Secret Path" className="flex-grow" isRequired>
<SecretPathInput
{...field}
placeholder="Provide a path, default is /"
environment={selectedEnvSlug?.slug}
/>
</FormControl>
)}
/>
</div>
<div className="border-t border-mineshaft-600 pt-4">
<div className="mb-4 flex items-center justify-between">
<div>Secrets</div>
</div>
<div className="flex w-full items-start gap-3">
<Controller
control={control}
name="secrets"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className="flex-1"
isError={Boolean(error)}
errorText={error?.message}
>
<FilterableSelect
placeholder={
// eslint-disable-next-line no-nested-ternary
isSecretsLoading
? "Loading secrets..."
: secrets?.length
? "Select secrets..."
: "No secrets found..."
}
isLoading={isSecretsLoading}
options={secrets}
value={value}
onChange={onChange}
isMulti
getOptionValue={(option) => option.key}
getOptionLabel={(option) => option.key}
/>
</FormControl>
)}
/>
<Tooltip content="Select All">
<IconButton
className="mt-1 h-9 w-9"
ariaLabel="Select all"
variant="outline_bg"
size="xs"
onClick={handleSecSelectAll}
>
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
</IconButton>
</Tooltip>
</div>
<div className="my-6 ml-2">
<Switch
id="populate-include-value"
isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) => setShouldIncludeValues(isChecked as boolean)}
>
Include secret values
</Switch>
</div>
<div className="flex items-center space-x-4">
<Button
leftIcon={<FontAwesomeIcon icon={faClone} />}
type="submit"
isDisabled={!isDirty}
>
Copy Secrets
</Button>
<Button variant="plain" colorSchema="secondary" onClick={() => onToggle(false)}>
Cancel
</Button>
</div>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,165 @@
import { useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faInfoCircle, faPaste } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { ProjectPermissionCan } from "@app/components/permissions";
import { parseDotEnv, parseJson } from "@app/components/utilities/parseSecrets";
import {
Button,
FormControl,
Modal,
ModalContent,
ModalTrigger,
TextArea
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
type Props = {
isOpen?: boolean;
isSmaller?: boolean;
onToggle: (isOpen: boolean) => void;
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
environment: string;
secretPath: string;
};
const formSchema = z.object({
value: z.string().trim()
});
type TForm = z.infer<typeof formSchema>;
const PasteEnvForm = ({ onParsedEnv }: Pick<Props, "onParsedEnv">) => {
const {
handleSubmit,
register,
formState: { isDirty, errors },
setError,
setFocus
} = useForm<TForm>({ defaultValues: { value: "" }, resolver: zodResolver(formSchema) });
const onSubmit = ({ value }: TForm) => {
let env: Record<string, { value: string; comments: string[] }>;
try {
env = parseJson(value);
} catch {
// not json, parse as env
env = parseDotEnv(value);
}
if (!Object.keys(env).length) {
setError("value", {
message: "No secrets found. Please make sure the provided format is valid."
});
setFocus("value");
return;
}
onParsedEnv(env);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormControl
label="Secret Values"
isError={Boolean(errors.value)}
errorText={errors.value?.message}
icon={<FontAwesomeIcon size="sm" className="text-mineshaft-400" icon={faInfoCircle} />}
tooltipClassName="max-w-lg px-2 whitespace-pre-line"
tooltipText={
<div className="flex flex-col gap-2">
<p>Example Formats:</p>
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
{/* eslint-disable-next-line react/jsx-no-comment-textnodes */}
<p className="text-mineshaft-400">// .json</p>
{JSON.stringify(
{
APP_NAME: "example-service",
APP_VERSION: "1.2.3",
NODE_ENV: "production"
},
null,
2
)}
</pre>
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
<p className="text-mineshaft-400"># .env</p>
<p>APP_NAME=&quot;example-service&quot;</p>
<p>APP_VERSION=&quot;1.2.3&quot;</p>
<p>NODE_ENV=&quot;production&quot;</p>
</pre>
<pre className="rounded-md bg-mineshaft-900 p-3 text-xs">
<p className="text-mineshaft-400"># .yml</p>
<p>APP_NAME: example-service</p>
<p>APP_VERSION: 1.2.3</p>
<p>NODE_ENV: production</p>
</pre>
</div>
}
>
<TextArea
{...register("value")}
placeholder="Paste secrets in .json, .yml or .env format..."
className="h-[60vh] !resize-none"
/>
</FormControl>
<Button isDisabled={!isDirty} type="submit">
Import Secrets
</Button>
</form>
);
};
export const PasteSecretEnvModal = ({
isSmaller,
isOpen,
onParsedEnv,
onToggle,
environment,
secretPath
}: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onToggle}>
<ModalTrigger asChild>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faPaste} />}
onClick={() => onToggle(true)}
isDisabled={!isAllowed}
variant="star"
size={isSmaller ? "xs" : "sm"}
>
Paste Secrets
</Button>
)}
</ProjectPermissionCan>
</div>
</ModalTrigger>
<ModalContent
className="max-w-2xl"
title="Past Secret Values"
subTitle="Paste values in .env, .json or .yml format"
>
<PasteEnvForm
onParsedEnv={(value) => {
onToggle(false);
onParsedEnv(value);
}}
/>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,401 @@
import { ChangeEvent, DragEvent } from "react";
import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability";
import { faPlus, faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
import { parseDotEnv, parseJson } from "@app/components/utilities/parseSecrets";
import { Button, Modal, ModalContent } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp, useToggle } from "@app/hooks";
import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api";
import {
dashboardKeys,
fetchDashboardProjectSecretsByKeys
} from "@app/hooks/api/dashboard/queries";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { SecretType } from "@app/hooks/api/types";
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
import { PasteSecretEnvModal } from "./PasteSecretEnvModal";
type TParsedEnv = Record<string, { value: string; comments: string[] }>;
type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv };
type Props = {
isSmaller: boolean;
environments?: { name: string; slug: string }[];
workspaceId: string;
environment: string;
secretPath: string;
isProtectedBranch?: boolean;
};
export const SecretDropzone = ({
isSmaller,
environments = [],
workspaceId,
environment,
secretPath,
isProtectedBranch = false
}: Props): JSX.Element => {
const { t } = useTranslation();
const [isDragActive, setDragActive] = useToggle();
const [isLoading, setIsLoading] = useToggle();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"importSecEnv",
"confirmUpload",
"pasteSecEnv"
] as const);
const queryClient = useQueryClient();
const { openPopUp } = usePopUpAction();
const { mutateAsync: updateSecretBatch, isPending: isUpdatingSecrets } = useUpdateSecretBatch({
options: { onSuccess: undefined }
});
const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({
options: { onSuccess: undefined }
});
const isSubmitting = isCreatingSecrets || isUpdatingSecrets;
const handleDrag = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive.on();
} else if (e.type === "dragleave") {
setDragActive.off();
}
};
const handleParsedEnv = async (env: TParsedEnv) => {
const envSecretKeys = Object.keys(env);
if (!envSecretKeys.length) {
createNotification({
type: "error",
text: "Failed to find secrets"
});
return;
}
try {
setIsLoading.on();
const { secrets: existingSecrets } = await fetchDashboardProjectSecretsByKeys({
secretPath,
environment,
projectId: workspaceId,
keys: envSecretKeys
});
const secretsGroupedByKey = existingSecrets.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.secretKey]: true }),
{}
);
const updateSecrets = Object.keys(env)
.filter((secKey) => secretsGroupedByKey[secKey])
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
const createSecrets = Object.keys(env)
.filter((secKey) => !secretsGroupedByKey[secKey])
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
handlePopUpOpen("confirmUpload", {
update: updateSecrets,
create: createSecrets
});
} catch (e) {
console.error(e);
createNotification({
text: "Failed to check for secret conflicts",
type: "error"
});
handlePopUpClose("confirmUpload");
} finally {
setIsLoading.off();
}
};
const parseFile = (file?: File, isJson?: boolean) => {
const reader = new FileReader();
if (!file) {
createNotification({
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
type: "error"
});
return;
}
// const fileType = file.name.split('.')[1];
setIsLoading.on();
reader.onload = (event) => {
if (!event?.target?.result) return;
// parse function's argument looks like to be ArrayBuffer
const env = isJson
? parseJson(event.target.result as ArrayBuffer)
: parseDotEnv(event.target.result as ArrayBuffer);
setIsLoading.off();
handleParsedEnv(env);
};
// If something is wrong show an error
try {
reader.readAsText(file);
} catch (error) {
console.log(error);
}
};
const handleDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!e.dataTransfer) {
return;
}
e.dataTransfer.dropEffect = "copy";
setDragActive.off();
parseFile(e.dataTransfer.files[0], e.dataTransfer.files[0].type === "application/json");
};
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json");
};
const handleSaveSecrets = async () => {
const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt;
try {
if (Object.keys(create || {}).length) {
await createSecretBatch({
secretPath,
workspaceId,
environment,
secrets: Object.entries(create).map(([secretKey, secData]) => ({
type: SecretType.Shared,
secretComment: secData.comments.join("\n"),
secretValue: secData.value,
secretKey
}))
});
}
if (Object.keys(update || {}).length) {
await updateSecretBatch({
secretPath,
workspaceId,
environment,
secrets: Object.entries(update).map(([secretKey, secData]) => ({
type: SecretType.Shared,
secretComment: secData.comments.join("\n"),
secretValue: secData.value,
secretKey
}))
});
}
queryClient.invalidateQueries({
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
});
queryClient.invalidateQueries({
queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
});
queryClient.invalidateQueries({
queryKey: secretApprovalRequestKeys.count({ workspaceId })
});
handlePopUpClose("confirmUpload");
createNotification({
type: "success",
text: isProtectedBranch
? "Uploaded changes have been sent for review"
: "Successfully uploaded secrets"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to upload secrets"
});
}
};
const createSecretCount = Object.keys(
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {}
).length;
const updateSecretCount = Object.keys(
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.update || {}
).length;
const isNonConflictingUpload = !updateSecretCount;
return (
<div>
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
className={twMerge(
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 px-2 py-4 text-sm text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
isDragActive && "opacity-100",
!isSmaller && "mx-auto w-full max-w-3xl flex-col space-y-4 py-20",
isLoading && "bg-bunker-800"
)}
>
{isLoading ? (
<div className="mb-16 flex items-center justify-center pt-16">
<img
src="/images/loading/loading.gif"
height={70}
width={120}
decoding="async"
loading="lazy"
alt="loading animation"
/>
</div>
) : (
<div className="flex flex-col items-center justify-center space-y-2">
<div>
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
</div>
<div>
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<input
id="fileSelect"
disabled={!isAllowed}
type="file"
className="absolute h-full w-full cursor-pointer opacity-0"
accept=".txt,.env,.yml,.yaml,.json"
onChange={handleFileUpload}
/>
)}
</ProjectPermissionCan>
<div
className={twMerge(
"flex w-full flex-row items-center justify-center py-4",
isSmaller && "py-1"
)}
>
<div className="w-1/5 border-t border-mineshaft-700" />
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
<div className="w-1/5 border-t border-mineshaft-700" />
</div>
<div className="flex flex-col items-center justify-center gap-4 lg:flex-row">
<PasteSecretEnvModal
isOpen={popUp.pasteSecEnv.isOpen}
onToggle={(isOpen) => handlePopUpToggle("pasteSecEnv", isOpen)}
onParsedEnv={handleParsedEnv}
environment={environment}
secretPath={secretPath}
isSmaller={isSmaller}
/>
<CopySecretsFromBoard
isOpen={popUp.importSecEnv.isOpen}
onToggle={(isOpen) => handlePopUpToggle("importSecEnv", isOpen)}
onParsedEnv={handleParsedEnv}
environment={environment}
environments={environments}
workspaceId={workspaceId}
secretPath={secretPath}
isSmaller={isSmaller}
/>
{!isSmaller && (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: "*",
secretTags: ["*"]
})}
>
{(isAllowed) => (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
variant="star"
isDisabled={!isAllowed}
>
Add a New Secret
</Button>
)}
</ProjectPermissionCan>
)}
</div>
</div>
)}
</div>
<Modal
isOpen={popUp?.confirmUpload?.isOpen}
onOpenChange={(open) => handlePopUpToggle("confirmUpload", open)}
>
<ModalContent
title="Confirm Secret Upload"
footerContent={[
<Button
isLoading={isSubmitting}
isDisabled={isSubmitting}
colorSchema={isNonConflictingUpload ? "primary" : "danger"}
key="overwrite-btn"
onClick={handleSaveSecrets}
>
{isNonConflictingUpload ? "Upload" : "Overwrite"}
</Button>,
<Button
key="keep-old-btn"
className="ml-4"
onClick={() => handlePopUpClose("confirmUpload")}
variant="outline_bg"
isDisabled={isSubmitting}
>
Cancel
</Button>
]}
>
{isNonConflictingUpload ? (
<div>
Are you sure you want to import {createSecretCount} secret
{createSecretCount > 1 ? "s" : ""} to this environment?
</div>
) : (
<div className="flex flex-col text-gray-300">
<div>Your project already contains the following {updateSecretCount} secrets:</div>
<div className="mt-2 text-sm text-gray-400">
{Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {})
?.map((key) => key)
.join(", ")}
</div>
<div className="mt-6">
Are you sure you want to overwrite these secrets
{createSecretCount > 0
? ` and import ${createSecretCount} new
one${createSecretCount > 1 ? "s" : ""}`
: ""}
?
</div>
</div>
)}
</ModalContent>
</Modal>
</div>
);
};
@@ -0,0 +1 @@
export { SecretDropzone } from "./SecretDropzone";