From 48cb5f6e9bbd42765bb88d204cf9a8c3fde3bea3 Mon Sep 17 00:00:00 2001 From: McPizza0 Date: Tue, 3 Dec 2024 23:25:27 +0000 Subject: [PATCH 1/2] feat(notifications): add copyable request IDs --- .../notifications/Notifications.tsx | 38 ++++++++++++- .../components/v2/CopyButton/CopyButton.tsx | 57 +++++++++++++++++++ .../src/components/v2/CopyButton/index.tsx | 2 + .../src/components/v2/Tooltip/Tooltip.tsx | 6 +- frontend/src/hooks/api/dashboard/queries.tsx | 42 ++++++++++++-- frontend/src/hooks/api/secrets/queries.tsx | 31 ++++++++-- frontend/src/reactQuery.tsx | 41 ++++++++----- 7 files changed, 190 insertions(+), 27 deletions(-) create mode 100644 frontend/src/components/v2/CopyButton/CopyButton.tsx create mode 100644 frontend/src/components/v2/CopyButton/index.tsx diff --git a/frontend/src/components/notifications/Notifications.tsx b/frontend/src/components/notifications/Notifications.tsx index 23b4eebaa..41e8194ea 100644 --- a/frontend/src/components/notifications/Notifications.tsx +++ b/frontend/src/components/notifications/Notifications.tsx @@ -1,18 +1,54 @@ 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; + cta?: ReactNode; + copyActions?: { icon?: IconDefinition; value: string; name: string; label?: string }[]; }; -export const NotificationContent = ({ title, text, children }: TNotification) => { +export const NotificationContent = ({ title, text, children, cta, copyActions }: TNotification) => { return (
{title &&
{title}
}
{text}
{children &&
{children}
} + {(cta || copyActions) && ( +
+ {cta} + + {copyActions && ( +
+ {copyActions.map((action) => ( +
+ {action.label && ( + {action.label} + )} + +
+ ))} +
+ )} +
+ )}
); }; diff --git a/frontend/src/components/v2/CopyButton/CopyButton.tsx b/frontend/src/components/v2/CopyButton/CopyButton.tsx new file mode 100644 index 000000000..63db5901e --- /dev/null +++ b/frontend/src/components/v2/CopyButton/CopyButton.tsx @@ -0,0 +1,57 @@ +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 tooltipText = name ? `Copy ${name}` : "Copy to clipboard"; + + const [copyText, isCopying, setCopyText] = useTimedReset({ + initialState: tooltipText + }); + + async function handleCopyText() { + setCopyText("Copied"); + navigator.clipboard.writeText(value); + } + + return ( +
+ + { + handleCopyText(); + }} + > + + + +
+ ); +}; + +CopyButton.displayName = "CopyButton"; diff --git a/frontend/src/components/v2/CopyButton/index.tsx b/frontend/src/components/v2/CopyButton/index.tsx new file mode 100644 index 000000000..9a7bc5991 --- /dev/null +++ b/frontend/src/components/v2/CopyButton/index.tsx @@ -0,0 +1,2 @@ +export type { CopyButtonProps } from "./CopyButton"; +export { CopyButton } from "./CopyButton"; diff --git a/frontend/src/components/v2/Tooltip/Tooltip.tsx b/frontend/src/components/v2/Tooltip/Tooltip.tsx index e02b9fc38..abfebc5b4 100644 --- a/frontend/src/components/v2/Tooltip/Tooltip.tsx +++ b/frontend/src/components/v2/Tooltip/Tooltip.tsx @@ -13,6 +13,7 @@ export type TooltipProps = Omit // 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 )} > diff --git a/frontend/src/hooks/api/dashboard/queries.tsx b/frontend/src/hooks/api/dashboard/queries.tsx index adff8bb0e..3aa599716 100644 --- a/frontend/src/hooks/api/dashboard/queries.tsx +++ b/frontend/src/hooks/api/dashboard/queries.tsx @@ -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}` + } + ] }); } }, diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index b3b3a4164..a803ff50b 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -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(); } }, diff --git a/frontend/src/reactQuery.tsx b/frontend/src/reactQuery.tsx index c897c6e02..c5e3d0e3b 100644 --- a/frontend/src/reactQuery.tsx +++ b/frontend/src/reactQuery.tsx @@ -32,13 +32,8 @@ export const queryClient = new QueryClient({ { title: "Validation Error", type: "error", - text: ( -
-

Please check the input and try again.

-

Request ID: {serverResponse.requestId}

-
- ), - children: ( + text: "Please check the input and try again.", + cta: (