feat(infisical-pg): completed root folder level secret, secret import and folder

This commit is contained in:
Akhil Mohan
2024-01-27 12:38:23 +05:30
parent 8a6ab7f2f6
commit 0e1191f2ea
70 changed files with 3105 additions and 278 deletions
+2 -1
View File
@@ -14,6 +14,7 @@ import {
TGetUserProjectPermissionDTO,
TOrgRole,
TPermission,
TProjectPermission,
TProjectRole
} from "./types";
@@ -50,7 +51,7 @@ const getProjectRoles = async (projectId: string) => {
}>(`/api/ee/v1/workspace/${projectId}/roles`);
return data.data.roles.map(({ permissions, ...el }) => ({
...el,
permissions: unpackRules(permissions as PackRule<TPermission>[])
permissions: unpackRules(permissions as PackRule<TProjectPermission>[])
}));
};
+3 -3
View File
@@ -10,7 +10,7 @@ export type TProjectRole = {
createdAt: string;
updatedAt: string;
description?: string;
permissions: TPermission[];
permissions: TProjectPermission[];
};
export type TOrgRole = {
@@ -67,13 +67,13 @@ export type TCreateProjectRoleDTO = {
name: string;
description?: string;
slug: string;
permissions: TPermission[];
permissions: TProjectPermission[];
};
export type TUpdateProjectRoleDTO = {
projectId: string;
id: string;
} & Partial<Omit<TCreateOrgRoleDTO, "orgId">>;
} & Partial<Omit<TCreateProjectRoleDTO, "orgId">>;
export type TDeleteProjectRoleDTO = {
projectId: string;
@@ -20,25 +20,25 @@ import {
} from "./types";
const queryKeys = {
getSecretFolders: ({ workspaceId, environment, directory }: TGetProjectFoldersDTO) =>
["secret-folders", { workspaceId, environment, directory }] as const
getSecretFolders: ({ projectId, environment, path }: TGetProjectFoldersDTO) =>
["secret-folders", { projectId, environment, path }] as const
};
const fetchProjectFolders = async (workspaceId: string, environment: string, directory = "/") => {
const fetchProjectFolders = async (projectId: string, environment: string, path = "/") => {
const { data } = await apiRequest.get<{ folders: TSecretFolder[] }>("/api/v1/folders", {
params: {
workspaceId,
projectId,
environment,
directory
path
}
});
return data.folders;
};
export const useGetProjectFolders = ({
workspaceId,
projectId,
environment,
directory = "/",
path = "/",
options = {}
}: TGetProjectFoldersDTO & {
options?: Omit<
@@ -53,21 +53,21 @@ export const useGetProjectFolders = ({
}) =>
useQuery({
...options,
queryKey: queryKeys.getSecretFolders({ workspaceId, environment, directory }),
enabled: Boolean(workspaceId) && Boolean(environment) && (options?.enabled ?? true),
queryFn: async () => fetchProjectFolders(workspaceId, environment, directory)
queryKey: queryKeys.getSecretFolders({ projectId, environment, path }),
enabled: Boolean(projectId) && Boolean(environment) && (options?.enabled ?? true),
queryFn: async () => fetchProjectFolders(projectId, environment, path)
});
export const useGetFoldersByEnv = ({
directory = "/",
workspaceId,
path = "/",
projectId,
environments
}: TGetFoldersByEnvDTO) => {
const folders = useQueries({
queries: environments.map((environment) => ({
queryKey: queryKeys.getSecretFolders({ workspaceId, environment, directory }),
queryFn: async () => fetchProjectFolders(workspaceId, environment, directory),
enabled: Boolean(workspaceId) && Boolean(environment)
queryKey: queryKeys.getSecretFolders({ projectId, environment, path }),
queryFn: async () => fetchProjectFolders(projectId, environment, path),
enabled: Boolean(projectId) && Boolean(environment)
}))
});
@@ -105,15 +105,13 @@ export const useCreateFolder = () => {
const { data } = await apiRequest.post("/api/v1/folders", dto);
return data;
},
onSuccess: (_, { workspaceId, environment, directory }) => {
onSuccess: (_, { projectId, environment, path }) => {
queryClient.invalidateQueries(queryKeys.getSecretFolders({ projectId, environment, path }));
queryClient.invalidateQueries(
queryKeys.getSecretFolders({ workspaceId, environment, directory })
secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path })
);
queryClient.invalidateQueries(
secretSnapshotKeys.list({ workspaceId, environment, directory })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ workspaceId, environment, directory })
secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
);
}
});
@@ -123,24 +121,22 @@ export const useUpdateFolder = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateFolderDTO>({
mutationFn: async ({ directory = "/", folderName, name, environment, workspaceId }) => {
const { data } = await apiRequest.patch(`/api/v1/folders/${folderName}`, {
mutationFn: async ({ path = "/", folderId, name, environment, projectId }) => {
const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, {
name,
environment,
workspaceId,
directory
projectId,
path
});
return data;
},
onSuccess: (_, { workspaceId, environment, directory }) => {
onSuccess: (_, { projectId, environment, path }) => {
queryClient.invalidateQueries(queryKeys.getSecretFolders({ projectId, environment, path }));
queryClient.invalidateQueries(
queryKeys.getSecretFolders({ workspaceId, environment, directory })
secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path })
);
queryClient.invalidateQueries(
secretSnapshotKeys.list({ workspaceId, environment, directory })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ workspaceId, environment, directory })
secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
);
}
});
@@ -150,25 +146,23 @@ export const useDeleteFolder = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteFolderDTO>({
mutationFn: async ({ directory = "/", folderName, environment, workspaceId }) => {
const { data } = await apiRequest.delete(`/api/v1/folders/${folderName}`, {
mutationFn: async ({ path = "/", folderId, environment, projectId }) => {
const { data } = await apiRequest.delete(`/api/v1/folders/${folderId}`, {
data: {
environment,
workspaceId,
directory
projectId,
path
}
});
return data;
},
onSuccess: (_, { directory = "/", workspaceId, environment }) => {
onSuccess: (_, { path = "/", projectId, environment }) => {
queryClient.invalidateQueries(queryKeys.getSecretFolders({ projectId, environment, path }));
queryClient.invalidateQueries(
queryKeys.getSecretFolders({ workspaceId, environment, directory })
secretSnapshotKeys.list({ workspaceId: projectId, environment, directory: path })
);
queryClient.invalidateQueries(
secretSnapshotKeys.list({ workspaceId, environment, directory })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ workspaceId, environment, directory })
secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
);
}
});
+13 -13
View File
@@ -4,35 +4,35 @@ export type TSecretFolder = {
};
export type TGetProjectFoldersDTO = {
workspaceId: string;
projectId: string;
environment: string;
directory?: string;
path?: string;
};
export type TGetFoldersByEnvDTO = {
environments: string[];
workspaceId: string;
directory?: string;
projectId: string;
path?: string;
};
export type TCreateFolderDTO = {
workspaceId: string;
projectId: string;
environment: string;
folderName: string;
directory?: string;
name: string;
path?: string;
};
export type TUpdateFolderDTO = {
workspaceId: string;
projectId: string;
environment: string;
name: string;
folderName: string;
directory?: string;
folderId: string;
path?: string;
};
export type TDeleteFolderDTO = {
workspaceId: string;
projectId: string;
environment: string;
folderName: string;
directory?: string;
folderId: string;
path?: string;
};
@@ -9,21 +9,21 @@ export const useCreateSecretImport = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretImportDTO>({
mutationFn: async ({ secretImport, environment, workspaceId, directory }) => {
mutationFn: async ({ import: secretImport, environment, projectId, path }) => {
const { data } = await apiRequest.post("/api/v1/secret-imports", {
secretImport,
import: secretImport,
environment,
workspaceId,
directory
projectId,
path
});
return data;
},
onSuccess: (_, { workspaceId, environment, directory }) => {
onSuccess: (_, { environment, projectId, path }) => {
queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
secretImportKeys.getProjectSecretImports({ projectId, environment, path })
);
queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
secretImportKeys.getSecretImportSecrets({ projectId, environment, path })
);
}
});
@@ -33,21 +33,21 @@ export const useUpdateSecretImport = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretImportDTO>({
mutationFn: async ({ environment, workspaceId, directory, secretImports, id }) => {
const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, {
secretImports,
mutationFn: async ({ environment, import: secretImports, projectId, path, id }) => {
const { data } = await apiRequest.patch(`/api/v1/secret-imports/${id}`, {
import: secretImports,
environment,
workspaceId,
directory
path,
projectId
});
return data;
},
onSuccess: (_, { workspaceId, environment, directory }) => {
onSuccess: (_, { environment, projectId, path }) => {
queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
secretImportKeys.getProjectSecretImports({ projectId, path, environment })
);
queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
secretImportKeys.getSecretImportSecrets({ environment, path, projectId })
);
}
});
@@ -57,21 +57,22 @@ export const useDeleteSecretImport = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSecretImportDTO>({
mutationFn: async ({ id, secretImportEnv, secretImportPath }) => {
mutationFn: async ({ id, projectId, path, environment }) => {
const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, {
data: {
secretImportPath,
secretImportEnv
projectId,
path,
environment
}
});
return data;
},
onSuccess: (_, { workspaceId, environment, directory }) => {
onSuccess: (_, { projectId, environment, path }) => {
queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
secretImportKeys.getProjectSecretImports({ projectId, environment, path })
);
queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
secretImportKeys.getSecretImportSecrets({ projectId, environment, path })
);
}
});
@@ -7,44 +7,44 @@ import {
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { TGetImportedSecrets, TGetSecretImports, TImportedSecrets, TSecretImports } from "./types";
import { TGetImportedSecrets, TGetSecretImports, TImportedSecrets, TSecretImport } from "./types";
export const secretImportKeys = {
getProjectSecretImports: ({ environment, workspaceId, directory }: TGetSecretImports) =>
[{ workspaceId, directory, environment }, "secrets-imports"] as const,
getProjectSecretImports: ({ environment, projectId, path }: TGetSecretImports) =>
[{ projectId, path, environment }, "secrets-imports"] as const,
getSecretImportSecrets: ({
workspaceId,
environment,
directory
projectId,
path
}: Omit<TGetImportedSecrets, "decryptFileKey">) =>
[{ workspaceId, environment, directory }, "secrets-import-sec"] as const
[{ environment, path, projectId }, "secrets-import-sec"] as const
};
const fetchSecretImport = async ({ workspaceId, environment, directory }: TGetSecretImports) => {
const { data } = await apiRequest.get<{ secretImport: TSecretImports }>(
const fetchSecretImport = async ({ projectId, environment, path = "/" }: TGetSecretImports) => {
const { data } = await apiRequest.get<{ secretImports: TSecretImport[] }>(
"/api/v1/secret-imports",
{
params: {
workspaceId,
projectId,
environment,
directory
path
}
}
);
return data.secretImport;
return data.secretImports;
};
export const useGetSecretImports = ({
workspaceId,
environment,
directory = "/",
path = "/",
projectId,
options = {}
}: TGetSecretImports & {
options?: Omit<
UseQueryOptions<
TSecretImports,
TSecretImport[],
unknown,
TSecretImports,
TSecretImport[],
ReturnType<typeof secretImportKeys.getProjectSecretImports>
>,
"queryKey" | "queryFn"
@@ -52,9 +52,9 @@ export const useGetSecretImports = ({
}) =>
useQuery({
...options,
queryKey: secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory }),
enabled: Boolean(workspaceId) && Boolean(environment) && (options?.enabled ?? true),
queryFn: () => fetchSecretImport({ workspaceId, environment, directory })
queryKey: secretImportKeys.getProjectSecretImports({ environment, projectId, path }),
enabled: Boolean(projectId) && Boolean(environment) && (options?.enabled ?? true),
queryFn: () => fetchSecretImport({ path, projectId, environment })
});
const fetchImportedSecrets = async (
@@ -76,10 +76,10 @@ const fetchImportedSecrets = async (
};
export const useGetImportedSecrets = ({
workspaceId,
environment,
decryptFileKey,
directory,
path,
projectId,
options = {}
}: TGetImportedSecrets & {
options?: Omit<
@@ -94,16 +94,16 @@ export const useGetImportedSecrets = ({
}) =>
useQuery({
enabled:
Boolean(workspaceId) &&
Boolean(projectId) &&
Boolean(environment) &&
Boolean(decryptFileKey) &&
(options?.enabled ?? true),
queryKey: secretImportKeys.getSecretImportSecrets({
workspaceId,
environment,
directory
path,
projectId
}),
queryFn: () => fetchImportedSecrets(workspaceId, environment, directory),
queryFn: () => fetchImportedSecrets(projectId, environment, path),
select: useCallback(
(data: TImportedSecrets[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
+23 -20
View File
@@ -1,12 +1,16 @@
import { UserWsKeyPair } from "../keys/types";
import { EncryptedSecret } from "../secrets/types";
export type TSecretImports = {
export type TSecretImport = {
id: string;
workspaceId: string;
environment: string;
folderId: string;
imports: Array<{ environment: string; secretPath: string }>;
importPath: string;
importEnv: {
name: string;
slug: string;
id: string;
};
position: string;
createdAt: string;
updatedAt: string;
};
@@ -19,44 +23,43 @@ export type TImportedSecrets = {
};
export type TGetSecretImports = {
workspaceId: string;
projectId: string;
environment: string;
directory?: string;
path?: string;
};
export type TGetImportedSecrets = {
workspaceId: string;
projectId: string;
environment: string;
directory?: string;
path?: string;
decryptFileKey: UserWsKeyPair;
};
export type TCreateSecretImportDTO = {
workspaceId: string;
projectId: string;
environment: string;
directory?: string;
secretImport: {
path?: string;
import: {
environment: string;
secretPath: string;
path: string;
};
};
export type TUpdateSecretImportDTO = {
id: string;
workspaceId: string;
projectId: string;
environment: string;
directory?: string;
secretImports: Array<{
path?: string;
import: Partial<{
environment: string;
secretPath: string;
path: string;
position: number;
}>;
};
export type TDeleteSecretImportDTO = {
id: string;
workspaceId: string;
projectId: string;
environment: string;
directory?: string;
secretImportPath: string;
secretImportEnv: string;
path?: string;
};
+1 -1
View File
@@ -12,7 +12,7 @@ export type {
} from "./secretApprovalRequest/types";
export { ApprovalStatus, CommitType } from "./secretApprovalRequest/types";
export type { TSecretFolder } from "./secretFolders/types";
export type { TImportedSecrets, TSecretImports } from "./secretImports/types";
export type { TImportedSecrets, TSecretImport } from "./secretImports/types";
export type {
TGetSecretRotationProviders,
TProviderTemplate,
@@ -89,7 +89,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
</THead>
<TBody>
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
{(roles as TProjectRole[])?.map((role) => {
{roles?.map((role) => {
const { id, name, slug } = role;
const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
@@ -118,7 +118,7 @@ const multiEnvForm2Api = (
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
// if any of them is set in all push it without any condition
PERMISSION_ACTIONS.forEach((action) => {
if (formVal?.all?.[action]) permissions.push({ action, subject });
if (formVal?.all?.[action]) permissions.push({ action, subject: [subject] });
});
if (!isFullAccess) {
@@ -139,7 +139,7 @@ const multiEnvForm2Api = (
if (formVal[slug]?.secretPath)
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
permissions.push({ action, subject, conditions });
permissions.push({ action, subject: [subject], conditions });
}
});
});
@@ -156,7 +156,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
} else {
Object.entries(actions).forEach(([action, isAllowed]) => {
if (isAllowed) {
permissions.push({ subject: rule, action });
permissions.push({ subject: [rule], action });
}
});
}
@@ -88,9 +88,9 @@ export const SecretMainPage = () => {
});
// fetch folders
const { data: folders, isLoading: isFoldersLoading } = useGetProjectFolders({
workspaceId,
projectId: workspaceId,
environment,
directory: secretPath
path: secretPath
});
// fetch secret imports
const {
@@ -98,9 +98,9 @@ export const SecretMainPage = () => {
isLoading: isSecretImportsLoading,
isFetching: isSecretImportsFetching
} = useGetSecretImports({
workspaceId,
projectId: workspaceId,
environment,
directory: secretPath,
path: secretPath,
options: {
enabled: canReadSecret
}
@@ -108,15 +108,15 @@ export const SecretMainPage = () => {
// fetch imported secrets to show user the overriden ones
const { data: importedSecrets } = useGetImportedSecrets({
workspaceId,
projectId: workspaceId,
environment,
decryptFileKey: decryptFileKey!,
directory: secretPath,
path: secretPath,
options: {
enabled: canReadSecret
}
});
// fetch tags
// fech tags
const { data: tags } = useGetWsTags(canReadSecret ? workspaceId : "");
const { data: boardPolicy } = useGetSecretApprovalPolicyOfABoard({
@@ -146,7 +146,7 @@ export const SecretMainPage = () => {
isPaused: !canDoReadRollback
});
const isNotEmtpy = Boolean(secrets?.length || folders?.length || secretImports?.imports?.length);
const isNotEmtpy = Boolean(secrets?.length || folders?.length || secretImports?.length);
const handleSortToggle = () =>
setSortDir((state) => (state === SortDir.ASC ? SortDir.DESC : SortDir.ASC));
@@ -112,10 +112,10 @@ export const ActionBar = ({
const handleFolderCreate = async (folderName: string) => {
try {
await createFolder({
folderName,
directory: secretPath,
name: folderName,
path: secretPath,
environment,
workspaceId
projectId: workspaceId
});
handlePopUpClose("addFolder");
createNotification({
@@ -66,11 +66,11 @@ export const CreateSecretImportForm = ({
try {
await createSecretImport({
environment,
workspaceId,
directory: secretPath,
secretImport: {
projectId: workspaceId,
path: secretPath,
import: {
environment: importedEnv,
secretPath: importedSecPath
path: importedSecPath
}
});
onClose();
@@ -42,12 +42,13 @@ export const FolderListView = ({
const handleFolderUpdate = async (newFolderName: string) => {
try {
const { id: folderId } = popUp.updateFolder.data as TSecretFolder;
await updateFolder({
folderName: popUp.updateFolder.data as string,
folderId,
name: newFolderName,
directory: secretPath,
path: secretPath,
environment,
workspaceId
projectId: workspaceId
});
handlePopUpClose("updateFolder");
createNotification({
@@ -65,11 +66,12 @@ export const FolderListView = ({
const handleFolderDelete = async () => {
try {
const { id: folderId } = popUp.deleteFolder.data as TSecretFolder;
await deleteFolder({
folderName: popUp.deleteFolder.data as string,
directory: secretPath,
folderId,
path: secretPath,
environment,
workspaceId
projectId: workspaceId
});
handlePopUpClose("deleteFolder");
createNotification({
@@ -106,13 +108,13 @@ export const FolderListView = ({
.map(({ name, id }) => (
<div
key={id}
className="flex group border-b border-mineshaft-600 hover:bg-mineshaft-700 cursor-pointer"
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
>
<div className="w-11 px-5 py-3 text-yellow-700 flex items-center">
<div className="flex w-11 items-center px-5 py-3 text-yellow-700">
<FontAwesomeIcon icon={faFolder} />
</div>
<div
className="flex-grow px-4 py-3 flex items-center"
className="flex flex-grow items-center px-4 py-3"
role="button"
tabIndex={0}
onKeyDown={(evt) => {
@@ -122,7 +124,7 @@ export const FolderListView = ({
>
{name}
</div>
<div className="px-3 py-3 flex items-center space-x-4 border-l border-mineshaft-600">
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-3 py-3">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
@@ -134,8 +136,8 @@ export const FolderListView = ({
ariaLabel="edit-folder"
variant="plain"
size="sm"
className="group-hover:opacity-100 opacity-0 p-0"
onClick={() => handlePopUpOpen("updateFolder", name)}
className="p-0 opacity-0 group-hover:opacity-100"
onClick={() => handlePopUpOpen("updateFolder", { id, name })}
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPencilSquare} size="lg" />
@@ -153,8 +155,8 @@ export const FolderListView = ({
ariaLabel="delete-folder"
variant="plain"
size="md"
className="group-hover:opacity-100 opacity-0 p-0"
onClick={() => handlePopUpOpen("deleteFolder", name)}
className="p-0 opacity-0 group-hover:opacity-100"
onClick={() => handlePopUpOpen("deleteFolder", { id, name })}
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faClose} size="lg" />
@@ -171,14 +173,14 @@ export const FolderListView = ({
<ModalContent title="Edit Folder">
<FolderForm
isEdit
defaultFolderName={popUp.updateFolder.data as string}
defaultFolderName={(popUp.updateFolder?.data as TSecretFolder)?.name}
onUpdateFolder={handleFolderUpdate}
/>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.deleteFolder.isOpen}
deleteKey={popUp.deleteFolder?.data as string}
deleteKey={(popUp.deleteFolder?.data as TSecretFolder)?.name}
title="Do you want to delete this folder?"
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
onDeleteApproved={handleFolderDelete}
@@ -12,17 +12,18 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { EmptyState, IconButton, SecretInput, TableContainer } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
type Props = {
onDelete: (environment: string, secretPath: string) => void;
onDelete: () => void;
environment: string;
secretPath?: string;
importedEnv: string;
importedSecPath: string;
importEnvName: string;
importEnvPath: string;
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
searchTerm: string;
id: string;
};
// to show the environment and folder icon
@@ -31,7 +32,7 @@ export const EnvFolderIcon = ({ env, secretPath }: { env: string; secretPath: st
<div style={{ minWidth: "96px" }}>{env || "-"}</div>
{secretPath && (
<div className="inline-flex items-center space-x-2 border-l border-mineshaft-600 pl-2">
<FontAwesomeIcon icon={faFolder} className="text-green-700 text-md" />
<FontAwesomeIcon icon={faFolder} className="text-md text-green-700" />
<span>{secretPath}</span>
</div>
)}
@@ -39,9 +40,10 @@ export const EnvFolderIcon = ({ env, secretPath }: { env: string; secretPath: st
);
export const SecretImportItem = ({
importedEnv,
importedSecPath,
onDelete,
id,
importEnvName,
importEnvPath,
importedSecrets = [],
searchTerm = "",
secretPath,
@@ -49,10 +51,8 @@ export const SecretImportItem = ({
}: Props) => {
const [isExpanded, setIsExpanded] = useToggle();
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
id: `${importedEnv}-${importedSecPath}`
id
});
const { currentWorkspace } = useWorkspace();
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
useEffect(() => {
const filteredSecrets = importedSecrets.filter((secret) =>
@@ -80,7 +80,7 @@ export const SecretImportItem = ({
return (
<>
<div
className="flex group border-b border-mineshaft-600 hover:bg-mineshaft-700 cursor-pointer"
className="group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
role="button"
ref={setNodeRef}
tabIndex={0}
@@ -88,13 +88,13 @@ export const SecretImportItem = ({
onClick={() => setIsExpanded.toggle()}
onKeyDown={() => setIsExpanded.toggle()}
>
<div className="w-12 px-4 py-2 flex items-center text-green-700">
<div className="flex w-12 items-center px-4 py-2 text-green-700">
<FontAwesomeIcon icon={faFileImport} />
</div>
<div className="flex-grow px-4 py-2 flex items-center">
<EnvFolderIcon env={rowEnv?.name || ""} secretPath={importedSecPath} />
<div className="flex flex-grow items-center px-4 py-2">
<EnvFolderIcon env={importEnvName || ""} secretPath={importEnvPath} />
</div>
<div className="px-4 py-2 flex items-center space-x-4 border-l border-mineshaft-600">
<div className="flex items-center space-x-4 border-l border-mineshaft-600 px-4 py-2">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
@@ -107,7 +107,7 @@ export const SecretImportItem = ({
colorSchema="primary"
variant="plain"
ariaLabel="expand"
className="group-hover:opacity-100 opacity-0 p-0"
className="p-0 opacity-0 group-hover:opacity-100"
{...attributes}
{...listeners}
isDisabled={!isAllowed}
@@ -128,10 +128,10 @@ export const SecretImportItem = ({
variant="plain"
colorSchema="danger"
ariaLabel="delete"
className="group-hover:opacity-100 opacity-0 p-0"
className="p-0 opacity-0 group-hover:opacity-100"
onClick={(evt) => {
evt.stopPropagation();
onDelete(importedEnv, importedSecPath);
onDelete();
}}
isDisabled={!isAllowed}
>
@@ -167,7 +167,7 @@ export const SecretImportItem = ({
{importedSecrets
.filter((secret) => secret.key.toUpperCase().includes(searchTerm.toUpperCase()))
.map(({ key, value, overriden }, index) => (
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
<tr key={`${id}-${key}-${index + 1}`}>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{key}
</td>
@@ -17,7 +17,7 @@ import { DeleteActionModal } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteSecretImport, useUpdateSecretImport } from "@app/hooks/api";
import { TSecretImports } from "@app/hooks/api/secretImports/types";
import { TSecretImport } from "@app/hooks/api/secretImports/types";
import { DecryptedSecret } from "@app/hooks/api/types";
import { SecretImportItem } from "./SecretImportItem";
@@ -76,17 +76,15 @@ type Props = {
environment: string;
workspaceId: string;
secretPath?: string;
secretImports?: TSecretImports;
secretImports?: TSecretImport[];
isFetching?: boolean;
secrets?: DecryptedSecret[];
importedSecrets?: TImportedSecrets;
searchTerm: string;
};
type TDeleteSecretImport = { environment: string; secretPath: string };
export const SecretImportListView = ({
secretImports,
secretImports = [],
environment,
workspaceId,
secretPath,
@@ -107,21 +105,11 @@ export const SecretImportListView = ({
useSensor(KeyboardSensor, {})
);
const [items, setItems] = useState(
(secretImports?.imports || [])?.map((dto) => ({
id: `${dto.environment}-${dto.secretPath}`,
...dto
}))
);
const [items, setItems] = useState(secretImports);
useEffect(() => {
if (!isFetching) {
setItems(
(secretImports?.imports || [])?.map((dto) => ({
id: `${dto.environment}-${dto.secretPath}`,
...dto
}))
);
setItems(secretImports);
}
}, [isFetching]);
@@ -129,24 +117,19 @@ export const SecretImportListView = ({
const { mutate: updateSecretImport } = useUpdateSecretImport();
const handleSecretImportDelete = async () => {
const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport
?.data as TDeleteSecretImport;
const { id: secretImportId } = popUp.deleteSecretImport?.data as { id: string };
try {
if (secretImports?.id) {
await deleteSecretImport({
workspaceId,
environment,
directory: secretPath,
id: secretImports?.id,
secretImportEnv: importEnv,
secretImportPath: impSecPath
});
handlePopUpClose("deleteSecretImport");
createNotification({
type: "success",
text: "Successfully removed secret link"
});
}
await deleteSecretImport({
projectId: workspaceId,
environment,
path: secretPath,
id: secretImportId
});
handlePopUpClose("deleteSecretImport");
createNotification({
type: "success",
text: "Successfully removed secret link"
});
} catch (err) {
console.error(err);
createNotification({
@@ -163,11 +146,13 @@ export const SecretImportListView = ({
const newImportOrder = arrayMove(items, oldIndex, newIndex);
setItems(newImportOrder);
updateSecretImport({
workspaceId,
projectId: workspaceId,
environment,
directory: secretPath,
id: secretImports?.id || "",
secretImports: newImportOrder
path: secretPath,
id: active.id as string,
import: {
position: newIndex + 1
}
});
}
};
@@ -181,26 +166,28 @@ export const SecretImportListView = ({
modifiers={[restrictToVerticalAxis]}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items?.map(({ secretPath: importedSecPath, environment: importedEnv }) => (
<SecretImportItem
searchTerm={searchTerm}
key={`${importedEnv}-${importedSecPath}`}
importedEnv={importedEnv}
importedSecPath={importedSecPath}
importedSecrets={computeImportedSecretRows(
importedEnv,
importedSecPath,
importedSecrets,
secrets,
environments
)}
secretPath={secretPath}
environment={environment}
onDelete={(env, secPath) =>
handlePopUpOpen("deleteSecretImport", { environment: env, secretPath: secPath })
}
/>
))}
{items?.map((item) => {
const { importPath, importEnv, id } = item;
return (
<SecretImportItem
searchTerm={searchTerm}
key={`imported-env-${id}`}
id={id}
importEnvPath={importPath}
importEnvName={importEnv.name}
importedSecrets={computeImportedSecretRows(
importEnv.slug,
importPath,
importedSecrets,
secrets,
environments
)}
secretPath={secretPath}
environment={environment}
onDelete={() => handlePopUpOpen("deleteSecretImport", item)}
/>
);
})}
</SortableContext>
</DndContext>
<DeleteActionModal
@@ -208,8 +195,8 @@ export const SecretImportListView = ({
deleteKey="unlink"
title="Do you want to remove this secret import?"
subTitle={`This will unlink secrets from environment ${
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
(popUp.deleteSecretImport?.data as TSecretImport)?.importEnv
} of path ${(popUp.deleteSecretImport?.data as TSecretImport)?.importPath}?`}
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
onDeleteApproved={handleSecretImportDelete}
/>
@@ -208,8 +208,8 @@ export const SecretListView = ({
} = modSecret;
const hasKeyChanged = oldKey !== key;
const tagIds = tags.map(({ id }) => id);
const oldTagIds = orgSecret.tags.map(({ id }) => id);
const tagIds = tags?.map(({ id }) => id);
const oldTagIds = (orgSecret?.tags || []).map(({ id }) => id);
const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds);
const isSharedSecUnchanged =
(
@@ -99,9 +99,9 @@ export const SecretOverviewPage = () => {
decryptFileKey: latestFileKey!
});
const { folders, folderNames, isFolderPresentInEnv } = useGetFoldersByEnv({
workspaceId,
environments: userAvailableEnvs.map(({ slug }) => slug),
directory: secretPath
projectId: workspaceId,
path: secretPath,
environments: userAvailableEnvs.map(({ slug }) => slug)
});
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
@@ -119,10 +119,10 @@ export const SecretOverviewPage = () => {
const folderName = pathSegment.at(-1);
if (folderName && parentPath) {
await createFolder({
workspaceId,
projectId: workspaceId,
path: secretPath,
environment: env,
directory: parentPath,
folderName
name: folderName
});
}
}
@@ -216,10 +216,10 @@ export const SecretOverviewPage = () => {
const folderName = path.at(-1);
if (folderName && directory) {
await createFolder({
workspaceId,
projectId: workspaceId,
environment: slug,
directory,
folderName
path: secretPath,
name: folderName
});
}
}