diff --git a/frontend/src/components/notifications/Notifications.tsx b/frontend/src/components/notifications/Notifications.tsx index 23b4eebaa..fdc35b9af 100644 --- a/frontend/src/components/notifications/Notifications.tsx +++ b/frontend/src/components/notifications/Notifications.tsx @@ -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 (
{title &&
{title}
}
{text}
{children &&
{children}
} + {(callToAction || copyActions) && ( +
+ {callToAction} + + {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..ff1161ca9 --- /dev/null +++ b/frontend/src/components/v2/CopyButton/CopyButton.tsx @@ -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({ + initialState: name ? `Copy ${name}` : "Copy to clipboard" + }); + + 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..8e5d674ca 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.", + callToAction: (