mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
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:
@ -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>
|
||||
);
|
||||
};
|
||||
|
55
frontend/src/components/v2/CopyButton/CopyButton.tsx
Normal file
55
frontend/src/components/v2/CopyButton/CopyButton.tsx
Normal 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";
|
2
frontend/src/components/v2/CopyButton/index.tsx
Normal file
2
frontend/src/components/v2/CopyButton/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export type { CopyButtonProps } from "./CopyButton";
|
||||
export { CopyButton } from "./CopyButton";
|
@ -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
|
||||
)}
|
||||
>
|
||||
|
@ -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}`
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
@ -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}`
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user