diff --git a/frontend/src/components/features/TtlFormLabel.tsx b/frontend/src/components/features/TtlFormLabel.tsx new file mode 100644 index 000000000..14382abb4 --- /dev/null +++ b/frontend/src/components/features/TtlFormLabel.tsx @@ -0,0 +1,36 @@ +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { FormLabel, Tooltip } from "../v2"; + +// To give users example of possible values of TTL +export const TtlFormLabel = ({ label }: { label: string }) => ( +
+ + 1m, 2h, 3d.{" "} + + More + + + } + > + + + } + /> +
+); diff --git a/frontend/src/components/features/index.tsx b/frontend/src/components/features/index.tsx new file mode 100644 index 000000000..f00724b65 --- /dev/null +++ b/frontend/src/components/features/index.tsx @@ -0,0 +1 @@ +export { TtlFormLabel } from "./TtlFormLabel"; diff --git a/frontend/src/components/v2/FormControl/FormControl.tsx b/frontend/src/components/v2/FormControl/FormControl.tsx index 9f4e33cac..35f85383d 100644 --- a/frontend/src/components/v2/FormControl/FormControl.tsx +++ b/frontend/src/components/v2/FormControl/FormControl.tsx @@ -7,21 +7,23 @@ import { twMerge } from "tailwind-merge"; export type FormLabelProps = { id?: string; isRequired?: boolean; + isOptional?:boolean; label?: ReactNode; icon?: ReactNode; className?: string; }; -export const FormLabel = ({ id, label, isRequired, icon, className }: FormLabelProps) => ( +export const FormLabel = ({ id, label, isRequired, icon, className,isOptional }: FormLabelProps) => ( {label} {isRequired && *} + {isOptional && - Optional} {icon && ( {icon} @@ -54,6 +56,7 @@ export const FormHelperText = ({ isError, text }: FormHelperTextProps) => ( export type FormControlProps = { id?: string; isRequired?: boolean; + isOptional?: boolean; isError?: boolean; label?: ReactNode; helperText?: ReactNode; @@ -66,6 +69,7 @@ export type FormControlProps = { export const FormControl = ({ children, isRequired, + isOptional, label, helperText, errorText, @@ -77,7 +81,13 @@ export const FormControl = ({ return (
{typeof label === "string" ? ( - + ) : ( label )} diff --git a/frontend/src/hooks/api/dynamicSecret/index.ts b/frontend/src/hooks/api/dynamicSecret/index.ts new file mode 100644 index 000000000..58011629e --- /dev/null +++ b/frontend/src/hooks/api/dynamicSecret/index.ts @@ -0,0 +1,2 @@ +export { useCreateDynamicSecret, useDeleteDynamicSecret, useUpdateDynamicSecret } from "./mutation"; +export { useGetDynamicSecretDetails,useGetDynamicSecrets } from "./queries"; diff --git a/frontend/src/hooks/api/dynamicSecret/mutation.ts b/frontend/src/hooks/api/dynamicSecret/mutation.ts new file mode 100644 index 000000000..0c317a2eb --- /dev/null +++ b/frontend/src/hooks/api/dynamicSecret/mutation.ts @@ -0,0 +1,62 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { dynamicSecretKeys } from "./queries"; +import { + TCreateDynamicSecretDTO, + TDeleteDynamicSecretDTO, + TDynamicSecret, + TUpdateDynamicSecretDTO +} from "./types"; + +export const useCreateDynamicSecret = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TCreateDynamicSecretDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post<{ dynamicSecret: TDynamicSecret }>( + "/api/v1/dynamic-secrets", + dto + ); + return data.dynamicSecret; + }, + onSuccess: (_, { path, environment, projectId }) => { + queryClient.invalidateQueries(dynamicSecretKeys.list({ path, projectId, environment })); + } + }); +}; + +export const useUpdateDynamicSecret = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TUpdateDynamicSecretDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.patch<{ dynamicSecret: TDynamicSecret }>( + `/api/v1/dynamic-secrets/${dto.slug}`, + dto + ); + return data.dynamicSecret; + }, + onSuccess: (_, { path, environment, projectId }) => { + queryClient.invalidateQueries(dynamicSecretKeys.list({ path, projectId, environment })); + } + }); +}; + +export const useDeleteDynamicSecret = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteDynamicSecretDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.delete<{ dynamicSecret: TDynamicSecret }>( + `/api/v1/dynamic-secrets/${dto.slug}`, + { data: dto } + ); + return data.dynamicSecret; + }, + onSuccess: (_, { path, environment, projectId }) => { + queryClient.invalidateQueries(dynamicSecretKeys.list({ path, projectId, environment })); + } + }); +}; diff --git a/frontend/src/hooks/api/dynamicSecret/queries.ts b/frontend/src/hooks/api/dynamicSecret/queries.ts new file mode 100644 index 000000000..84099d7d9 --- /dev/null +++ b/frontend/src/hooks/api/dynamicSecret/queries.ts @@ -0,0 +1,62 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TDetailsDynamicSecretDTO, TDynamicSecret, TListDynamicSecretDTO } from "./types"; + +export const dynamicSecretKeys = { + list: ({ + projectId, + environment, + path + }: Pick) => + [{ projectId, environment, path }, "dynamic-secrets"] as const, + details: ({ path, environment, projectId, slug }: TDetailsDynamicSecretDTO) => + [{ projectId, path, environment, slug }, "dynamic-secret-details"] as const +}; + +export const useGetDynamicSecrets = ({ projectId, environment, path }: TListDynamicSecretDTO) => { + return useQuery({ + queryKey: dynamicSecretKeys.list({ path, environment, projectId }), + enabled: Boolean(projectId && environment && path), + queryFn: async () => { + const { data } = await apiRequest.get<{ dynamicSecrets: TDynamicSecret[] }>( + "/api/v1/dynamic-secrets", + { + params: { + projectId, + environment, + path + } + } + ); + + return data.dynamicSecrets; + } + }); +}; + +export const useGetDynamicSecretDetails = ({ + projectId, + environment, + path, + slug +}: TDetailsDynamicSecretDTO) => { + return useQuery({ + queryKey: dynamicSecretKeys.details({ path, environment, projectId, slug }), + enabled: Boolean(projectId && environment && path && slug), + queryFn: async () => { + const { data } = await apiRequest.get<{ + dynamicSecret: TDynamicSecret & { inputs: unknown }; + }>(`/api/v1/dynamic-secrets/${slug}`, { + params: { + projectId, + environment, + path + } + }); + + return data.dynamicSecret; + } + }); +}; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts new file mode 100644 index 000000000..821b61cc4 --- /dev/null +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -0,0 +1,83 @@ +export enum DynamicSecretStatus { + Deleting = "Revocation in process", + FailedDeletion = "Failed to delete" +} +// TODO(akhilmhdh): When we switch to monorepo all the server api ts will be in a shared repo +export type TDynamicSecret = { + id: string; + slug: string; + type: DynamicSecretProviders; + createdAt: string; + updatedAt: string; + defaultTTL: string; + status?: DynamicSecretStatus; + statusDetails?: string; + maxTTL: string; +}; + +export enum DynamicSecretProviders { + SqlDatabase = "sql-database" +} + +export enum SqlProviders { + Postgres = "postgres" +} + +export type TDynamicSecretProvider = { + type: DynamicSecretProviders; + inputs: { + client: SqlProviders; + host: string; + port: number; + database: string; + username: string; + password: string; + creationStatement: string; + revocationStatement: string; + renewStatement: string; + ca?: string | undefined; + }; +}; + +export type TCreateDynamicSecretDTO = { + projectId: string; + provider: TDynamicSecretProvider; + defaultTTL: string; + maxTTL?: string; + path: string; + environment: string; + slug: string; +}; + +export type TUpdateDynamicSecretDTO = { + slug: string; + projectId: string; + path: string; + environment: string; + data: { + newSlug?: string; + defaultTTL?: string; + maxTTL?: string | null; + inputs?: unknown; + }; +}; + +export type TListDynamicSecretDTO = { + projectId: string; + path: string; + environment: string; +}; + +export type TDeleteDynamicSecretDTO = { + projectId: string; + path: string; + environment: string; + slug: string; +}; + +export type TDetailsDynamicSecretDTO = { + projectId: string; + path: string; + environment: string; + slug: string; +}; diff --git a/frontend/src/hooks/api/dynamicSecretLease/index.ts b/frontend/src/hooks/api/dynamicSecretLease/index.ts new file mode 100644 index 000000000..7f2d6852f --- /dev/null +++ b/frontend/src/hooks/api/dynamicSecretLease/index.ts @@ -0,0 +1,6 @@ +export { + useCreateDynamicSecretLease, + useRenewDynamicSecretLease, + useRevokeDynamicSecretLease +} from "./mutation"; +export { useGetDynamicSecretLeases } from "./queries"; diff --git a/frontend/src/hooks/api/dynamicSecretLease/mutation.ts b/frontend/src/hooks/api/dynamicSecretLease/mutation.ts new file mode 100644 index 000000000..c24e3ed31 --- /dev/null +++ b/frontend/src/hooks/api/dynamicSecretLease/mutation.ts @@ -0,0 +1,72 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { dynamicSecretLeaseKeys } from "./queries"; +import { + TCreateDynamicSecretLeaseDTO, + TDynamicSecretLease, + TRenewDynamicSecretLeaseDTO, + TRevokeDynamicSecretLeaseDTO +} from "./types"; + +export const useCreateDynamicSecretLease = () => { + const queryClient = useQueryClient(); + + return useMutation< + { lease: TDynamicSecretLease; data: unknown }, + {}, + TCreateDynamicSecretLeaseDTO + >({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post<{ lease: TDynamicSecretLease; data: unknown }>( + "/api/v1/dynamic-secrets/leases", + dto + ); + return data; + }, + onSuccess: (_, { path, environment, projectId, slug }) => { + queryClient.invalidateQueries( + dynamicSecretLeaseKeys.list({ path, projectId, environment, slug }) + ); + } + }); +}; + +export const useRenewDynamicSecretLease = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TRenewDynamicSecretLeaseDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post<{ lease: TDynamicSecretLease }>( + `/api/v1/dynamic-secrets/leases/${dto.leaseId}/renew`, + dto + ); + return data.lease; + }, + onSuccess: (_, { path, environment, projectId, slug }) => { + queryClient.invalidateQueries( + dynamicSecretLeaseKeys.list({ path, projectId, environment, slug }) + ); + } + }); +}; + +export const useRevokeDynamicSecretLease = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TRevokeDynamicSecretLeaseDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.delete<{ lease: TDynamicSecretLease }>( + `/api/v1/dynamic-secrets/leases/${dto.leaseId}`, + { data: dto } + ); + return data.lease; + }, + onSuccess: (_, { path, environment, projectId, slug }) => { + queryClient.invalidateQueries( + dynamicSecretLeaseKeys.list({ path, projectId, environment, slug }) + ); + } + }); +}; diff --git a/frontend/src/hooks/api/dynamicSecretLease/queries.ts b/frontend/src/hooks/api/dynamicSecretLease/queries.ts new file mode 100644 index 000000000..0de9620c7 --- /dev/null +++ b/frontend/src/hooks/api/dynamicSecretLease/queries.ts @@ -0,0 +1,37 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TDynamicSecretLease, TListDynamicSecretLeaseDTO } from "./types"; + +export const dynamicSecretLeaseKeys = { + list: ({ projectId, environment, path, slug }: TListDynamicSecretLeaseDTO) => + [{ projectId, environment, path, slug }, "dynamic-secret-leases"] as const +}; + +export const useGetDynamicSecretLeases = ({ + projectId, + environment, + path, + slug, + enabled = true +}: TListDynamicSecretLeaseDTO) => { + return useQuery({ + queryKey: dynamicSecretLeaseKeys.list({ path, environment, projectId, slug }), + enabled: Boolean(projectId && environment && path && slug && enabled), + queryFn: async () => { + const { data } = await apiRequest.get<{ leases: TDynamicSecretLease[] }>( + `/api/v1/dynamic-secrets/leases/${slug}`, + { + params: { + projectId, + environment, + path + } + } + ); + + return data.leases; + } + }); +}; diff --git a/frontend/src/hooks/api/dynamicSecretLease/types.ts b/frontend/src/hooks/api/dynamicSecretLease/types.ts new file mode 100644 index 000000000..b888970c2 --- /dev/null +++ b/frontend/src/hooks/api/dynamicSecretLease/types.ts @@ -0,0 +1,47 @@ +export enum DynamicSecretLeaseStatus { + FailedDeletion = "Failed to delete" +} + +export type TDynamicSecretLease = { + id: string; + version: number; + expireAt: string; + dynamicSecretId: string; + status?: DynamicSecretLeaseStatus; + statusDetails?: string; + createdAt: string; + updatedAt: string; +}; + +export type TCreateDynamicSecretLeaseDTO = { + slug: string; + projectId: string; + ttl?: string; + path: string; + environment: string; +}; + +export type TRenewDynamicSecretLeaseDTO = { + leaseId: string; + slug: string; + ttl?: string; + projectId: string; + path: string; + environment: string; +}; + +export type TListDynamicSecretLeaseDTO = { + slug: string; + projectId: string; + path: string; + environment: string; + enabled?: boolean; +}; + +export type TRevokeDynamicSecretLeaseDTO = { + leaseId: string; + slug: string; + projectId: string; + path: string; + environment: string; +}; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 4ec8db3ac..48d85c285 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -3,6 +3,8 @@ export * from "./apiKeys"; export * from "./auditLogs"; export * from "./auth"; export * from "./bots"; +export * from "./dynamicSecret"; +export * from "./dynamicSecretLease"; export * from "./identities"; export * from "./incidentContacts"; export * from "./integrationAuth"; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 98fbe2413..578e9ffc6 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -3,4 +3,5 @@ export { useLeaveConfirm } from "./useLeaveConfirm"; export { usePersistentState } from "./usePersistentState"; export { usePopUp } from "./usePopUp"; export { useSyntaxHighlight } from "./useSyntaxHighlight"; +export { useTimedReset } from "./useTimedReset"; export { useToggle } from "./useToggle"; diff --git a/frontend/src/hooks/useTimedReset.tsx b/frontend/src/hooks/useTimedReset.tsx new file mode 100644 index 000000000..60e286978 --- /dev/null +++ b/frontend/src/hooks/useTimedReset.tsx @@ -0,0 +1,25 @@ +import { Dispatch, SetStateAction, useEffect, useState } from "react"; + +type Props = { + initialState: T; + delay?: number; +}; + +// this hook is used when you need to reset the state to previous one after a particular time +// usecase#1: To make copy to copied and back to copy in clipboard operation +export const useTimedReset = ({ + delay = 2000, + initialState +}: Props): [T, boolean, Dispatch>] => { + const [state, setState] = useState(initialState); + useEffect(() => { + let timer: NodeJS.Timeout; + if (state !== initialState) { + timer = setTimeout(() => setState(initialState), delay); + } + return () => clearTimeout(timer); + }, [state]); + + // state, isChaning, setState + return [state, state !== initialState, setState]; +};