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: (
- )
+ ),
+ 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 ? (
- ) : 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}`
+ }
+ ]
});
}
}