Merge pull request #2839 from Infisical/omar/eng-1966-click-to-copy-req-id-on-toast

Improvement(notifications): Add copyable request IDs to server side errors
This commit is contained in:
Daniel Hougaard
2024-12-05 04:04:33 +04:00
committed by GitHub
7 changed files with 194 additions and 27 deletions

View File

@ -1,18 +1,60 @@
import { ReactNode } from "react";
import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toastify";
import { faCopy, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { CopyButton } from "../v2/CopyButton";
export type TNotification = {
title?: string;
text: ReactNode;
children?: ReactNode;
callToAction?: ReactNode;
copyActions?: { icon?: IconDefinition; value: string; name: string; label?: string }[];
};
export const NotificationContent = ({ title, text, children }: TNotification) => {
export const NotificationContent = ({
title,
text,
children,
callToAction,
copyActions
}: TNotification) => {
return (
<div className="msg-container">
{title && <div className="text-md mb-1 font-medium">{title}</div>}
<div className={title ? "text-sm text-neutral-400" : "text-md"}>{text}</div>
{children && <div className="mt-2">{children}</div>}
{(callToAction || copyActions) && (
<div
className={twMerge(
"mt-2 flex h-7 w-full flex-row items-end gap-2",
callToAction ? "justify-between" : "justify-end"
)}
>
{callToAction}
{copyActions && (
<div className="flex h-7 flex-row items-center gap-2">
{copyActions.map((action) => (
<div className="flex flex-row items-center gap-2" key={`copy-${action.name}`}>
{action.label && (
<span className="ml-2 text-xs text-mineshaft-400">{action.label}</span>
)}
<CopyButton
value={action.value}
name={action.name}
size="xs"
variant="plain"
color="text-mineshaft-400"
icon={action.icon ?? faCopy}
/>
</div>
))}
</div>
)}
</div>
)}
</div>
);
};

View File

@ -0,0 +1,55 @@
import { faCheck, faCopy, IconDefinition } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { useTimedReset } from "@app/hooks";
import { IconButton } from "../IconButton";
import { Tooltip } from "../Tooltip";
export type CopyButtonProps = {
value: string;
size?: "xs" | "sm" | "md" | "lg";
variant?: "solid" | "outline" | "plain" | "star" | "outline_bg";
color?: string;
name?: string;
icon?: IconDefinition;
};
export const CopyButton = ({
value,
size = "sm",
variant = "solid",
color,
name,
icon = faCopy
}: CopyButtonProps) => {
const [copyText, isCopying, setCopyText] = useTimedReset<string>({
initialState: name ? `Copy ${name}` : "Copy to clipboard"
});
async function handleCopyText() {
setCopyText("Copied");
navigator.clipboard.writeText(value);
}
return (
<div>
<Tooltip content={copyText} size={size === "xs" || size === "sm" ? "sm" : "md"}>
<IconButton
ariaLabel={copyText}
variant={variant}
className={twMerge("group relative", color)}
size={size}
onClick={() => {
handleCopyText();
}}
>
<FontAwesomeIcon icon={isCopying ? faCheck : icon} />
</IconButton>
</Tooltip>
</div>
);
};
CopyButton.displayName = "CopyButton";

View File

@ -0,0 +1,2 @@
export type { CopyButtonProps } from "./CopyButton";
export { CopyButton } from "./CopyButton";

View File

@ -13,6 +13,7 @@ export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "
position?: "top" | "bottom" | "left" | "right";
isDisabled?: boolean;
center?: boolean;
size?: "sm" | "md";
};
export const Tooltip = ({
@ -26,6 +27,7 @@ export const Tooltip = ({
asChild = true,
isDisabled,
position = "top",
size = "md",
...props
}: TooltipProps) =>
// just render children if tooltip content is empty
@ -43,7 +45,7 @@ export const Tooltip = ({
sideOffset={5}
{...props}
className={twMerge(
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
`z-50 max-w-[15rem] select-none border border-mineshaft-600 bg-mineshaft-800 font-light text-bunker-200 shadow-md
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
@ -51,6 +53,8 @@ export const Tooltip = ({
`,
isDisabled && "!hidden",
center && "text-center",
size === "sm" && "rounded-sm py-1 px-2 text-xs",
size === "md" && "rounded-md py-2 px-4 text-sm",
className
)}
>

View File

@ -177,11 +177,21 @@ export const useGetProjectSecretsOverview = (
}),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
const { message, requestId } = error.response?.data as {
message: string;
requestId: string;
};
createNotification({
title: "Error fetching secret details",
type: "error",
text: serverResponse.message
text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
});
}
},
@ -270,11 +280,21 @@ export const useGetProjectSecretsDetails = (
}),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
const { message, requestId } = error.response?.data as {
message: string;
requestId: string;
};
createNotification({
title: "Error fetching secret details",
type: "error",
text: serverResponse.message
text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
});
}
},
@ -355,11 +375,21 @@ export const useGetProjectSecretsQuickSearch = (
}),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
const { message, requestId } = error.response?.data as {
message: string;
requestId: string;
};
createNotification({
title: "Error fetching secrets deep search",
type: "error",
text: serverResponse.message
text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
});
}
},

View File

@ -117,11 +117,21 @@ export const useGetProjectSecrets = ({
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
onError: (error) => {
if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string };
const { message, requestId } = error.response?.data as {
message: string;
requestId: string;
};
createNotification({
title: "Error fetching secrets",
type: "error",
text: serverResponse.message
text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
});
}
},
@ -148,15 +158,24 @@ export const useGetProjectSecretsAllEnv = ({
enabled: Boolean(workspaceId && environment),
onError: (error: unknown) => {
if (axios.isAxiosError(error) && !isErrorHandled) {
const serverResponse = error.response?.data as { message: string };
if (serverResponse.message !== ERROR_NOT_ALLOWED_READ_SECRETS) {
const { message, requestId } = error.response?.data as {
message: string;
requestId: string;
};
if (message !== ERROR_NOT_ALLOWED_READ_SECRETS) {
createNotification({
title: "Error fetching secrets",
type: "error",
text: serverResponse.message
text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
});
}
setIsErrorHandled.on();
}
},

View File

@ -32,13 +32,8 @@ export const queryClient = new QueryClient({
{
title: "Validation Error",
type: "error",
text: (
<div>
<p>Please check the input and try again.</p>
<p className="mt-2 text-xs">Request ID: {serverResponse.requestId}</p>
</div>
),
children: (
text: "Please check the input and try again.",
callToAction: (
<Modal>
<ModalTrigger>
<Button variant="outline_bg" size="xs">
@ -66,7 +61,14 @@ export const queryClient = new QueryClient({
</TableContainer>
</ModalContent>
</Modal>
)
),
copyActions: [
{
value: serverResponse.requestId,
name: "Request ID",
label: `Request ID: ${serverResponse.requestId}`
}
]
},
{ closeOnClick: false }
);
@ -77,9 +79,8 @@ export const queryClient = new QueryClient({
{
title: "Forbidden Access",
type: "error",
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`,
children: serverResponse?.details?.length ? (
text: `${serverResponse.message}.`,
callToAction: serverResponse?.details?.length ? (
<Modal>
<ModalTrigger>
<Button variant="outline_bg" size="xs">
@ -165,7 +166,14 @@ export const queryClient = new QueryClient({
</div>
</ModalContent>
</Modal>
) : undefined
) : undefined,
copyActions: [
{
value: serverResponse.requestId,
name: "Request ID",
label: `Request ID: ${serverResponse.requestId}`
}
]
},
{ closeOnClick: false }
);
@ -174,7 +182,14 @@ export const queryClient = new QueryClient({
createNotification({
title: "Bad Request",
type: "error",
text: `${serverResponse.message} [requestId=${serverResponse.requestId}]`
text: `${serverResponse.message}.`,
copyActions: [
{
value: serverResponse.requestId,
name: "Request ID",
label: `Request ID: ${serverResponse.requestId}`
}
]
});
}
}