Merge remote-tracking branch 'origin/main' into feat-secret-input-autocomplete

This commit is contained in:
Sheen Capadngan
2024-04-23 11:02:45 +08:00
939 changed files with 30576 additions and 10095 deletions
@@ -16,8 +16,8 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
@@ -54,7 +54,9 @@ import {
useCreateFolder,
useCreateSecretV3,
useDeleteSecretV3,
useGetDynamicSecretsOfAllEnv,
useGetFoldersByEnv,
useGetImportedSecretsAllEnvs,
useGetProjectSecretsAllEnv,
useGetUserWsKey,
useUpdateSecretV3
@@ -65,12 +67,13 @@ import { FolderForm } from "../SecretMainPage/components/ActionBar/FolderForm";
import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSection";
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
export const SecretOverviewPage = () => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const router = useRouter();
// this is to set expandable table width
@@ -97,6 +100,7 @@ export const SecretOverviewPage = () => {
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
const { currentOrg } = useOrganization();
const workspaceId = currentWorkspace?.id as string;
const projectSlug = currentWorkspace?.slug as string;
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
const [searchFilter, setSearchFilter] = useState("");
const secretPath = (router.query?.secretPath as string) || "/";
@@ -131,6 +135,20 @@ export const SecretOverviewPage = () => {
environments: userAvailableEnvs.map(({ slug }) => slug)
});
const { isImportedSecretPresentInEnv } = useGetImportedSecretsAllEnvs({
projectId: workspaceId,
decryptFileKey: latestFileKey!,
path: secretPath,
environments: userAvailableEnvs.map(({ slug }) => slug)
});
const { dynamicSecretNames, dynamicSecrets, isDynamicSecretPresentInEnv } =
useGetDynamicSecretsOfAllEnv({
projectSlug,
environmentSlugs: userAvailableEnvs.map(({ slug }) => slug),
path: secretPath
});
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
@@ -323,18 +341,27 @@ export const SecretOverviewPage = () => {
);
const canViewOverviewPage = Boolean(userAvailableEnvs.length);
// This is needed to also show imports from other paths – right now those are missing.
// const combinedKeys = [...secKeys, ...secretImports.map((impSecrets) => impSecrets?.data?.map((impSec) => impSec.secrets?.map((impSecKey) => impSecKey.key))).flat().flat()];
const filteredSecretNames = secKeys
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
const filteredFolderNames = folderNames?.filter((name) =>
name.toLowerCase().includes(searchFilter.toLowerCase())
);
const filteredDynamicSecrets = dynamicSecretNames?.filter((name) =>
name.toLowerCase().includes(searchFilter.toLowerCase())
);
const isTableEmpty =
!(
folders?.every(({ isLoading }) => isLoading) && secrets?.every(({ isLoading }) => isLoading)
folders?.every(({ isLoading }) => isLoading) &&
secrets?.every(({ isLoading }) => isLoading) &&
dynamicSecrets?.every(({ isLoading }) => isLoading)
) &&
filteredSecretNames?.length === 0 &&
filteredFolderNames?.length === 0;
filteredFolderNames?.length === 0 &&
filteredDynamicSecrets?.length === 0;
return (
<>
@@ -644,11 +671,21 @@ export const SecretOverviewPage = () => {
onClick={handleFolderClick}
/>
))}
{!isTableLoading &&
filteredDynamicSecrets.map((dynamicSecretName, index) => (
<SecretOverviewDynamicSecretRow
dynamicSecretName={dynamicSecretName}
isDynamicSecretInEnv={isDynamicSecretPresentInEnv}
environments={visibleEnvs}
key={`overview-${dynamicSecretName}-${index + 1}`}
/>
))}
{!isTableLoading &&
visibleEnvs?.length > 0 &&
filteredSecretNames.map((key, index) => (
<SecretOverviewTableRow
secretPath={secretPath}
isImportedSecretPresentInEnv={isImportedSecretPresentInEnv}
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
@@ -64,7 +64,7 @@ export const CreateSecretForm = ({
const workspaceId = currentWorkspace?.id || "";
const environments = currentWorkspace?.environments || [];
const { createNotification } = useNotificationContext();
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
@@ -1 +1 @@
export { CreateSecretForm } from "./CreateSecretForm"
export { CreateSecretForm } from "./CreateSecretForm";
@@ -1 +1 @@
export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection";
export { ProjectIndexSecretsSection } from "./ProjectIndexSecretsSection";
@@ -0,0 +1,50 @@
import { faCheck, faFingerprint, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Td, Tr } from "@app/components/v2";
type Props = {
dynamicSecretName: string;
environments: { name: string; slug: string }[];
isDynamicSecretInEnv: (name: string, env: string) => boolean;
};
export const SecretOverviewDynamicSecretRow = ({
dynamicSecretName,
environments = [],
isDynamicSecretInEnv
}: Props) => {
return (
<Tr isHoverable isSelectable className="group">
<Td className="sticky left-0 z-10 border-0 bg-mineshaft-800 bg-clip-padding p-0 group-hover:bg-mineshaft-700">
<div className="flex items-center space-x-5 border-r border-mineshaft-600 px-5 py-2.5">
<div className="text-yellow-700">
<FontAwesomeIcon icon={faFingerprint} />
</div>
<div>{dynamicSecretName}</div>
</div>
</Td>
{environments.map(({ slug }, i) => {
const isPresent = isDynamicSecretInEnv(dynamicSecretName, slug);
return (
<Td
key={`sec-overview-${slug}-${i + 1}-folder`}
className={twMerge(
"border-r border-mineshaft-600 py-3 group-hover:bg-mineshaft-700",
isPresent ? "text-green-600" : "text-red-600"
)}
>
<div className="flex justify-center">
<FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary
icon={isPresent ? faCheck : faXmark}
/>
</div>
</Td>
);
})}
</Tr>
);
};
@@ -0,0 +1 @@
export { SecretOverviewDynamicSecretRow } from "./SecretOverviewDynamicSecretRow";
@@ -15,6 +15,7 @@ export const SecretOverviewFolderRow = ({
folderName,
environments = [],
isFolderPresentInEnv,
onClick
}: Props) => {
return (
@@ -29,6 +30,7 @@ export const SecretOverviewFolderRow = ({
</Td>
{environments.map(({ slug }, i) => {
const isPresent = isFolderPresentInEnv(folderName, slug);
return (
<Td
key={`sec-overview-${slug}-${i + 1}-folder`}
@@ -38,7 +40,10 @@ export const SecretOverviewFolderRow = ({
)}
>
<div className="flex justify-center">
<FontAwesomeIcon icon={isPresent ? faCheck : faXmark} />
<FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary
icon={isPresent ? faCheck : faXmark}
/>
</div>
</Td>
);
@@ -2,8 +2,9 @@ import { Controller, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput/InfisicalSecretInput";
@@ -16,6 +17,7 @@ type Props = {
secretId?: string;
isCreatable?: boolean;
isVisible?: boolean;
isImportedSecret: boolean;
environment: string;
secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
@@ -26,6 +28,7 @@ type Props = {
export const SecretEditRow = ({
defaultValue,
isCreatable,
isImportedSecret,
onSecretUpdate,
secretName,
onSecretCreate,
@@ -47,7 +50,6 @@ export const SecretEditRow = ({
}
});
const [isDeleting, setIsDeleting] = useToggle();
const { createNotification } = useNotificationContext();
const handleFormReset = () => {
reset();
@@ -91,6 +93,7 @@ export const SecretEditRow = ({
<div className="group flex w-full cursor-text items-center space-x-2">
<div className="flex-grow border-r border-r-mineshaft-600 pr-2 pl-1">
<Controller
disabled={isImportedSecret}
control={control}
name="value"
render={({ field }) => (
@@ -100,11 +103,17 @@ export const SecretEditRow = ({
isVisible={isVisible}
secretPath={secretPath}
environment={environment}
isImport={isImportedSecret}
/>
)}
/>
</div>
<div className="flex w-16 justify-center space-x-3 pl-2 transition-all">
<div
className={twMerge(
"flex w-16 justify-center space-x-3 pl-2 transition-all",
isImportedSecret && "pointer-events-none opacity-0"
)}
>
{isDirty ? (
<>
<ProjectPermissionCan
@@ -4,6 +4,7 @@ import {
faCheck,
faEye,
faEyeSlash,
faFileImport,
faKey,
faXmark
} from "@fortawesome/free-solid-svg-icons";
@@ -26,6 +27,7 @@ type Props = {
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
isImportedSecretPresentInEnv: (name: string, env: string, secretName: string) => boolean;
};
export const SecretOverviewTableRow = ({
@@ -36,6 +38,7 @@ export const SecretOverviewTableRow = ({
onSecretUpdate,
onSecretCreate,
onSecretDelete,
isImportedSecretPresentInEnv,
expandableColWidth
}: Props) => {
const [isFormExpanded, setIsFormExpanded] = useToggle();
@@ -61,6 +64,9 @@ export const SecretOverviewTableRow = ({
</Td>
{environments.map(({ slug }, i) => {
const secret = getSecretByKey(slug, secretKey);
const isSecretImported = isImportedSecretPresentInEnv(secretPath, slug, secretKey);
const isSecretPresent = Boolean(secret);
const isSecretEmpty = secret?.value === "";
return (
@@ -69,16 +75,29 @@ export const SecretOverviewTableRow = ({
className={twMerge(
"py-0 px-0 group-hover:bg-mineshaft-700",
isFormExpanded && "border-t-2 border-mineshaft-500",
isSecretPresent && !isSecretEmpty ? "text-green-600" : "",
isSecretPresent && isSecretEmpty ? "text-yellow" : "",
!isSecretPresent && !isSecretEmpty ? "text-red-600" : ""
(isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "",
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "",
!isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : ""
)}
>
<div className="h-full w-full border-r border-mineshaft-600 py-[0.85rem] px-5">
<div className="flex justify-center">
{!isSecretEmpty && (
<Tooltip content={isSecretPresent ? "Present secret" : "Missing secret"}>
<FontAwesomeIcon icon={isSecretPresent ? faCheck : faXmark} />
<Tooltip
center
content={
// eslint-disable-next-line no-nested-ternary
isSecretPresent
? "Present secret"
: isSecretImported
? "Imported secret"
: "Missing secret"
}
>
<FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary
icon={isSecretPresent ? faCheck : isSecretImported ? faFileImport : faXmark}
/>
</Tooltip>
)}
{isSecretEmpty && (
@@ -143,6 +162,12 @@ export const SecretOverviewTableRow = ({
const secret = getSecretByKey(slug, secretKey);
const isCreatable = !secret;
const isImportedSecret = isImportedSecretPresentInEnv(
secretPath,
slug,
secretKey
);
return (
<tr
key={`secret-expanded-${slug}-${secretKey}`}
@@ -163,6 +188,7 @@ export const SecretOverviewTableRow = ({
secretName={secretKey}
defaultValue={secret?.value}
secretId={secret?.id}
isImportedSecret={isImportedSecret}
isCreatable={isCreatable}
onSecretDelete={onSecretDelete}
onSecretCreate={onSecretCreate}
@@ -8,7 +8,7 @@ import { AnimatePresence, motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { createNotification } from "@app/components/notifications";
import { IconButton, Input, Spinner, Tooltip } from "@app/components/v2";
import {
ProjectPermissionActions,
@@ -37,7 +37,7 @@ type TFormSchema = z.infer<typeof formSchema>;
function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }: Props) {
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const { createNotification } = useNotificationContext();
const secrets = environments.map((env) => getSecretByKey(env.slug, secretKey));