feat(ui): added dynamic secret and lease api hook

This commit is contained in:
Akhil Mohan
2024-03-21 20:20:24 +05:30
parent ab48c3b4fe
commit 177cd385cc
14 changed files with 449 additions and 3 deletions

View File

@@ -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 }) => (
<div>
<FormLabel
label={label}
icon={
<Tooltip
content={
<span>
1m, 2h, 3d.{" "}
<a
href="https://github.com/vercel/ms?tab=readme-ov-file#examples"
target="_blank"
rel="noopener noreferrer"
className="text-primary-700"
>
More
</a>
</span>
}
>
<FontAwesomeIcon
icon={faQuestionCircle}
size="sm"
className="relative bottom-1 right-1"
/>
</Tooltip>
}
/>
</div>
);

View File

@@ -0,0 +1 @@
export { TtlFormLabel } from "./TtlFormLabel";

View File

@@ -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.Root
className={twMerge(
"mb-0.5 ml-1 block flex items-center text-sm font-normal text-mineshaft-400",
"mb-0.5 ml-1 flex items-center text-sm font-normal text-mineshaft-400",
className
)}
htmlFor={id}
>
{label}
{isRequired && <span className="ml-1 text-red">*</span>}
{isOptional && <span className="ml-1 text-gray-500 italic text-xs">- Optional</span>}
{icon && (
<span className="ml-2 cursor-default text-mineshaft-300 hover:text-mineshaft-200">
{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 (
<div className={twMerge("mb-4", className)}>
{typeof label === "string" ? (
<FormLabel label={label} isRequired={isRequired} id={id} icon={icon} />
<FormLabel
label={label}
isOptional={isOptional}
isRequired={isRequired}
id={id}
icon={icon}
/>
) : (
label
)}

View File

@@ -0,0 +1,2 @@
export { useCreateDynamicSecret, useDeleteDynamicSecret, useUpdateDynamicSecret } from "./mutation";
export { useGetDynamicSecretDetails,useGetDynamicSecrets } from "./queries";

View File

@@ -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 }));
}
});
};

View File

@@ -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<TListDynamicSecretDTO, "path" | "environment" | "projectId">) =>
[{ 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;
}
});
};

View File

@@ -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;
};

View File

@@ -0,0 +1,6 @@
export {
useCreateDynamicSecretLease,
useRenewDynamicSecretLease,
useRevokeDynamicSecretLease
} from "./mutation";
export { useGetDynamicSecretLeases } from "./queries";

View File

@@ -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 })
);
}
});
};

View File

@@ -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;
}
});
};

View File

@@ -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;
};

View File

@@ -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";

View File

@@ -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";

View File

@@ -0,0 +1,25 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
type Props<T extends unknown> = {
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 = <T extends string | number | boolean>({
delay = 2000,
initialState
}: Props<T>): [T, boolean, Dispatch<SetStateAction<T>>] => {
const [state, setState] = useState<T>(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];
};