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 { ReactNode } from "react";
import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toastify"; 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 = { export type TNotification = {
title?: string; title?: string;
text: ReactNode; text: ReactNode;
children?: 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 ( return (
<div className="msg-container"> <div className="msg-container">
{title && <div className="text-md mb-1 font-medium">{title}</div>} {title && <div className="text-md mb-1 font-medium">{title}</div>}
<div className={title ? "text-sm text-neutral-400" : "text-md"}>{text}</div> <div className={title ? "text-sm text-neutral-400" : "text-md"}>{text}</div>
{children && <div className="mt-2">{children}</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> </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"; position?: "top" | "bottom" | "left" | "right";
isDisabled?: boolean; isDisabled?: boolean;
center?: boolean; center?: boolean;
size?: "sm" | "md";
}; };
export const Tooltip = ({ export const Tooltip = ({
@@ -26,6 +27,7 @@ export const Tooltip = ({
asChild = true, asChild = true,
isDisabled, isDisabled,
position = "top", position = "top",
size = "md",
...props ...props
}: TooltipProps) => }: TooltipProps) =>
// just render children if tooltip content is empty // just render children if tooltip content is empty
@@ -43,7 +45,7 @@ export const Tooltip = ({
sideOffset={5} sideOffset={5}
{...props} {...props}
className={twMerge( 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=top]:animate-slideDownAndFade
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
@@ -51,6 +53,8 @@ export const Tooltip = ({
`, `,
isDisabled && "!hidden", isDisabled && "!hidden",
center && "text-center", center && "text-center",
size === "sm" && "rounded-sm py-1 px-2 text-xs",
size === "md" && "rounded-md py-2 px-4 text-sm",
className className
)} )}
> >

View File

@@ -177,11 +177,21 @@ export const useGetProjectSecretsOverview = (
}), }),
onError: (error) => { onError: (error) => {
if (axios.isAxiosError(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({ createNotification({
title: "Error fetching secret details", title: "Error fetching secret details",
type: "error", 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) => { onError: (error) => {
if (axios.isAxiosError(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({ createNotification({
title: "Error fetching secret details", title: "Error fetching secret details",
type: "error", 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) => { onError: (error) => {
if (axios.isAxiosError(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({ createNotification({
title: "Error fetching secrets deep search", title: "Error fetching secrets deep search",
type: "error", 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 }), queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
onError: (error) => { onError: (error) => {
if (axios.isAxiosError(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({ createNotification({
title: "Error fetching secrets", title: "Error fetching secrets",
type: "error", 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), enabled: Boolean(workspaceId && environment),
onError: (error: unknown) => { onError: (error: unknown) => {
if (axios.isAxiosError(error) && !isErrorHandled) { if (axios.isAxiosError(error) && !isErrorHandled) {
const serverResponse = error.response?.data as { message: string }; const { message, requestId } = error.response?.data as {
if (serverResponse.message !== ERROR_NOT_ALLOWED_READ_SECRETS) { message: string;
requestId: string;
};
if (message !== ERROR_NOT_ALLOWED_READ_SECRETS) {
createNotification({ createNotification({
title: "Error fetching secrets", title: "Error fetching secrets",
type: "error", type: "error",
text: serverResponse.message text: message,
copyActions: [
{
value: requestId,
name: "Request ID",
label: `Request ID: ${requestId}`
}
]
}); });
} }
setIsErrorHandled.on(); setIsErrorHandled.on();
} }
}, },

View File

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