diff --git a/frontend/src/hooks/api/dynamicSecret/index.ts b/frontend/src/hooks/api/dynamicSecret/index.ts index 58011629e..3d6e403bc 100644 --- a/frontend/src/hooks/api/dynamicSecret/index.ts +++ b/frontend/src/hooks/api/dynamicSecret/index.ts @@ -1,2 +1,6 @@ export { useCreateDynamicSecret, useDeleteDynamicSecret, useUpdateDynamicSecret } from "./mutation"; -export { useGetDynamicSecretDetails,useGetDynamicSecrets } from "./queries"; +export { + useGetDynamicSecretDetails, + useGetDynamicSecrets, + useGetDynamicSecretsOfAllEnv +} from "./queries"; diff --git a/frontend/src/hooks/api/dynamicSecret/queries.ts b/frontend/src/hooks/api/dynamicSecret/queries.ts index 37f851bed..481b431cc 100644 --- a/frontend/src/hooks/api/dynamicSecret/queries.ts +++ b/frontend/src/hooks/api/dynamicSecret/queries.ts @@ -1,8 +1,14 @@ -import { useQuery } from "@tanstack/react-query"; +import { useCallback, useMemo } from "react"; +import { useQueries, useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TDetailsDynamicSecretDTO, TDynamicSecret, TListDynamicSecretDTO } from "./types"; +import { + TDetailsDynamicSecretDTO, + TDynamicSecret, + TGetDynamicSecretsByEnvsDTO, + TListDynamicSecretDTO +} from "./types"; export const dynamicSecretKeys = { list: ({ @@ -15,7 +21,11 @@ export const dynamicSecretKeys = { [{ projectSlug, path, environmentSlug, name }, "dynamic-secret-details"] as const }; -export const useGetDynamicSecrets = ({ projectSlug, environmentSlug, path }: TListDynamicSecretDTO) => { +export const useGetDynamicSecrets = ({ + projectSlug, + environmentSlug, + path +}: TListDynamicSecretDTO) => { return useQuery({ queryKey: dynamicSecretKeys.list({ path, environmentSlug, projectSlug }), enabled: Boolean(projectSlug && environmentSlug && path), @@ -60,3 +70,57 @@ export const useGetDynamicSecretDetails = ({ } }); }; + +export const useGetDynamicSecretsOfAllEnv = ({ + path, + projectSlug, + environmentSlugs +}: TGetDynamicSecretsByEnvsDTO) => { + const dynamicSecrets = useQueries({ + queries: environmentSlugs.map((environment) => ({ + queryKey: dynamicSecretKeys.list({ path, environmentSlug: environment, projectSlug }), + enabled: Boolean(projectSlug && environment && path), + queryFn: async () => { + const { data } = await apiRequest.get<{ dynamicSecrets: TDynamicSecret[] }>( + "/api/v1/dynamic-secrets", + { + params: { + projectSlug, + environmentSlug: environment, + path + } + } + ); + + return data.dynamicSecrets; + } + })) + }); + + const dynamicSecretNames = useMemo(() => { + const names = new Set(); + dynamicSecrets?.forEach(({ data }) => { + data?.forEach(({ name }) => { + names.add(name); + }); + }); + return [...names]; + }, [(dynamicSecrets || []).map((dynamicSecret) => dynamicSecret.data)]); + + const isDynamicSecretPresentInEnv = useCallback( + (name: string, env: string) => { + const selectedEnvIndex = environmentSlugs.indexOf(env); + if (selectedEnvIndex !== -1) { + return Boolean( + dynamicSecrets?.[selectedEnvIndex]?.data?.find( + ({ name: dynamicSecretName }) => dynamicSecretName === name + ) + ); + } + return false; + }, + [(dynamicSecrets || []).map((el) => el.data)] + ); + + return { dynamicSecrets, isDynamicSecretPresentInEnv, dynamicSecretNames }; +}; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 27d3a756c..b68ad33a1 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -82,3 +82,9 @@ export type TDetailsDynamicSecretDTO = { environmentSlug: string; name: string; }; + +export type TGetDynamicSecretsByEnvsDTO = { + projectSlug: string; + path: string; + environmentSlugs: string[]; +}; diff --git a/frontend/src/hooks/api/dynamicSecretLease/queries.ts b/frontend/src/hooks/api/dynamicSecretLease/queries.ts index 9084098d1..556c0edbf 100644 --- a/frontend/src/hooks/api/dynamicSecretLease/queries.ts +++ b/frontend/src/hooks/api/dynamicSecretLease/queries.ts @@ -35,3 +35,5 @@ export const useGetDynamicSecretLeases = ({ } }); }; + + diff --git a/frontend/src/hooks/api/dynamicSecretLease/types.ts b/frontend/src/hooks/api/dynamicSecretLease/types.ts index 76bedc8b3..e6fbb0e75 100644 --- a/frontend/src/hooks/api/dynamicSecretLease/types.ts +++ b/frontend/src/hooks/api/dynamicSecretLease/types.ts @@ -46,3 +46,4 @@ export type TRevokeDynamicSecretLeaseDTO = { environmentSlug: string; isForced?: boolean; }; + diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index c9680ad7b..03e4f50f6 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -54,6 +54,7 @@ import { useCreateFolder, useCreateSecretV3, useDeleteSecretV3, + useGetDynamicSecretsOfAllEnv, useGetFoldersByEnv, useGetImportedSecretsAllEnvs, useGetProjectSecretsAllEnv, @@ -66,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 router = useRouter(); // this is to set expandable table width @@ -98,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) || "/"; @@ -138,6 +141,13 @@ export const SecretOverviewPage = () => { 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(); @@ -336,12 +346,19 @@ export const SecretOverviewPage = () => { 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 ( <> @@ -651,6 +668,15 @@ export const SecretOverviewPage = () => { onClick={handleFolderClick} /> ))} + {!isTableLoading && + filteredDynamicSecrets.map((dynamicSecretName, index) => ( + + ))} {!isTableLoading && visibleEnvs?.length > 0 && filteredSecretNames.map((key, index) => ( diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow/SecretOverviewDynamicSecretRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow/SecretOverviewDynamicSecretRow.tsx new file mode 100644 index 000000000..f814b336b --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow/SecretOverviewDynamicSecretRow.tsx @@ -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 ( + + +
+
+ +
+
{dynamicSecretName}
+
+ + {environments.map(({ slug }, i) => { + const isPresent = isDynamicSecretInEnv(dynamicSecretName, slug); + + return ( + +
+ +
+ + ); + })} + + ); +}; diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow/index.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow/index.tsx new file mode 100644 index 000000000..f2375625b --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewDynamicSecretRow/index.tsx @@ -0,0 +1 @@ +export { SecretOverviewDynamicSecretRow } from "./SecretOverviewDynamicSecretRow";