mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +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 { 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
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";
|
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
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user