chore(frontend): Restyle toasts and replace all current instances with new one (#6854)

This commit is contained in:
sp.wack 2025-03-08 01:23:50 +04:00 committed by GitHub
parent a4908f9a75
commit 83851c398d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 161 additions and 109 deletions

View File

@ -2,11 +2,13 @@ import { createRoutesStub } from "react-router";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { screen, waitFor } from "@testing-library/react";
import toast from "react-hot-toast";
import App from "#/routes/_oh.app/route";
import OpenHands from "#/api/open-hands";
import * as CustomToast from "#/utils/custom-toast-handlers";
describe("App", () => {
const errorToastSpy = vi.spyOn(CustomToast, "displayErrorToast");
const RouteStub = createRoutesStub([
{ Component: App, path: "/conversation/:conversationId" },
]);
@ -34,26 +36,19 @@ describe("App", () => {
await screen.findByTestId("app-route");
});
it(
"should call endSession if the user does not have permission to view conversation",
async () => {
const errorToastSpy = vi.spyOn(toast, "error");
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
it("should call endSession if the user does not have permission to view conversation", async () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
getConversationSpy.mockResolvedValue(null);
renderWithProviders(
<RouteStub initialEntries={["/conversation/9999"]} />,
);
getConversationSpy.mockResolvedValue(null);
renderWithProviders(<RouteStub initialEntries={["/conversation/9999"]} />);
await waitFor(() => {
expect(endSessionMock).toHaveBeenCalledOnce();
expect(errorToastSpy).toHaveBeenCalledOnce();
});
},
);
await waitFor(() => {
expect(endSessionMock).toHaveBeenCalledOnce();
expect(errorToastSpy).toHaveBeenCalledOnce();
});
});
it("should not call endSession if the user has permission", async () => {
const errorToastSpy = vi.spyOn(toast, "error");
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
getConversationSpy.mockResolvedValue({

View File

@ -1,8 +1,12 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { trackError, showErrorToast, showChatError } from "#/utils/error-handler";
import posthog from "posthog-js";
import toast from "react-hot-toast";
import {
trackError,
showErrorToast,
showChatError,
} from "#/utils/error-handler";
import * as Actions from "#/services/actions";
import * as CustomToast from "#/utils/custom-toast-handlers";
vi.mock("posthog-js", () => ({
default: {
@ -10,12 +14,6 @@ vi.mock("posthog-js", () => ({
},
}));
vi.mock("react-hot-toast", () => ({
default: {
error: vi.fn(),
},
}));
vi.mock("#/services/actions", () => ({
handleStatusMessage: vi.fn(),
}));
@ -38,9 +36,12 @@ describe("Error Handler", () => {
trackError(error);
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Test error"), {
error_source: "test",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Test error"),
{
error_source: "test",
},
);
});
it("should include additional metadata in PostHog event", () => {
@ -55,15 +56,19 @@ describe("Error Handler", () => {
trackError(error);
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Test error"), {
error_source: "test",
extra: "info",
details: { foo: "bar" },
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Test error"),
{
error_source: "test",
extra: "info",
details: { foo: "bar" },
},
);
});
});
describe("showErrorToast", () => {
const errorToastSpy = vi.spyOn(CustomToast, "displayErrorToast");
it("should log error and show toast", () => {
const error = {
message: "Toast error",
@ -73,12 +78,15 @@ describe("Error Handler", () => {
showErrorToast(error);
// Verify PostHog logging
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Toast error"), {
error_source: "toast-test",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Toast error"),
{
error_source: "toast-test",
},
);
// Verify toast was shown
expect(toast.error).toHaveBeenCalled();
expect(errorToastSpy).toHaveBeenCalled();
});
it("should include metadata in PostHog event when showing toast", () => {
@ -90,10 +98,13 @@ describe("Error Handler", () => {
showErrorToast(error);
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Toast error"), {
error_source: "toast-test",
context: "testing",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Toast error"),
{
error_source: "toast-test",
context: "testing",
},
);
});
it("should log errors from different sources with appropriate metadata", () => {
@ -104,10 +115,13 @@ describe("Error Handler", () => {
metadata: { id: "error.agent" },
});
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Agent error"), {
error_source: "agent-status",
id: "error.agent",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Agent error"),
{
error_source: "agent-status",
id: "error.agent",
},
);
showErrorToast({
message: "Server error",
@ -115,11 +129,14 @@ describe("Error Handler", () => {
metadata: { error_code: 500, details: "Internal error" },
});
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Server error"), {
error_source: "server",
error_code: 500,
details: "Internal error",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Server error"),
{
error_source: "server",
error_code: 500,
details: "Internal error",
},
);
});
it("should log feedback submission errors with conversation context", () => {
@ -130,11 +147,14 @@ describe("Error Handler", () => {
metadata: { conversationId: "123", error },
});
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Feedback submission failed"), {
error_source: "feedback",
conversationId: "123",
error,
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Feedback submission failed"),
{
error_source: "feedback",
conversationId: "123",
error,
},
);
});
});
@ -149,9 +169,12 @@ describe("Error Handler", () => {
showChatError(error);
// Verify PostHog logging
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Chat error"), {
error_source: "chat-test",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Chat error"),
{
error_source: "chat-test",
},
);
// Verify error message was shown in chat
expect(Actions.handleStatusMessage).toHaveBeenCalledWith({

View File

@ -1,5 +1,4 @@
import { useDispatch, useSelector } from "react-redux";
import toast from "react-hot-toast";
import React from "react";
import posthog from "posthog-js";
import { useParams } from "react-router";
@ -23,6 +22,7 @@ import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bott
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
import { downloadTrajectory } from "#/utils/download-files";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
function getEntryPoint(
hasRepository: boolean | null,
@ -98,7 +98,7 @@ export function ChatInterface() {
const onClickExportTrajectoryButton = () => {
if (!params.conversationId) {
toast.error("ConversationId unknown, cannot download trajectory");
displayErrorToast("ConversationId unknown, cannot download trajectory");
return;
}
@ -110,7 +110,7 @@ export function ChatInterface() {
);
},
onError: (error) => {
toast.error(error.message);
displayErrorToast(error.message);
},
});
};

View File

@ -2,7 +2,6 @@ import React from "react";
import { FaListUl } from "react-icons/fa";
import { useDispatch } from "react-redux";
import posthog from "posthog-js";
import toast from "react-hot-toast";
import { NavLink, useLocation } from "react-router";
import { useGitHubUser } from "#/hooks/query/use-github-user";
import { UserActions } from "./user-actions";
@ -22,6 +21,7 @@ import { ConversationPanelWrapper } from "../conversation-panel/conversation-pan
import { useLogout } from "#/hooks/mutation/use-logout";
import { useConfig } from "#/hooks/query/use-config";
import { cn } from "#/utils/utils";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { HIDE_LLM_SETTINGS } from "#/utils/feature-flags";
export function Sidebar() {
@ -58,7 +58,7 @@ export function Sidebar() {
) {
// We don't show toast errors for settings in the global error handler
// because we have a special case for 404 errors
toast.error(
displayErrorToast(
"Something went wrong while fetching settings. Please reload the page.",
);
} else if (settingsError?.status === 404) {

View File

@ -1,10 +1,10 @@
import React from "react";
import { MutateOptions } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { useSettings } from "#/hooks/query/use-settings";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { PostSettings, Settings } from "#/types/settings";
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
type SaveUserSettingsConfig = {
onSuccess: MutateOptions<void, Error, Partial<PostSettings>>["onSuccess"];
@ -47,7 +47,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
onSuccess: config?.onSuccess,
onError: (error) => {
const errorMessage = retrieveAxiosErrorMessage(error);
toast.error(errorMessage);
displayErrorToast(errorMessage);
},
});
};

View File

@ -1,8 +1,8 @@
import { useMutation } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { Feedback } from "#/api/open-hands.types";
import OpenHands from "#/api/open-hands";
import { useConversation } from "#/context/conversation-context";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
type SubmitFeedbackArgs = {
feedback: Feedback;
@ -14,7 +14,7 @@ export const useSubmitFeedback = () => {
mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
OpenHands.submitFeedback(conversationId, feedback),
onError: (error) => {
toast.error(error.message);
displayErrorToast(error.message);
},
});
};

View File

@ -1,5 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M5.69949 5.72974L7.91965 7.9505L8.35077 7.51999L6.13001 5.29922L8.35077 3.07907L7.92026 2.64795L5.69949 4.86871L3.47934 2.64795L3.04883 3.07907L5.26898 5.29922L3.04883 7.51938L3.47934 7.9505L5.69949 5.72974Z"
fill="black" />
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 13.0602L16.2123 16.7725L17.273 15.7118L13.5607 11.9995L17.273 8.28723L16.2123 7.22657L12.5 10.9389L8.78771 7.22656L7.72705 8.28722L11.4394 11.9995L7.72706 15.7119L8.78772 16.7725L12.5 13.0602Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 327 B

View File

@ -3,8 +3,8 @@ import {
QueryCache,
MutationCache,
} from "@tanstack/react-query";
import toast from "react-hot-toast";
import { retrieveAxiosErrorMessage } from "./utils/retrieve-axios-error-message";
import { displayErrorToast } from "./utils/custom-toast-handlers";
const shownErrors = new Set<string>();
export const queryClientConfig: QueryClientConfig = {
@ -14,7 +14,7 @@ export const queryClientConfig: QueryClientConfig = {
const errorMessage = retrieveAxiosErrorMessage(error);
if (!shownErrors.has(errorMessage)) {
toast.error(errorMessage || "An error occurred");
displayErrorToast(errorMessage || "An error occurred");
shownErrors.add(errorMessage);
setTimeout(() => {
@ -28,7 +28,7 @@ export const queryClientConfig: QueryClientConfig = {
onError: (error, _, __, mutation) => {
if (!mutation?.meta?.disableToast) {
const message = retrieveAxiosErrorMessage(error);
toast.error(message);
displayErrorToast(message);
}
},
}),

View File

@ -1,5 +1,4 @@
import React from "react";
import toast from "react-hot-toast";
import { useDispatch, useSelector } from "react-redux";
import { setImportedProjectZip } from "#/state/initial-query-slice";
import { RootState } from "#/store";
@ -7,6 +6,7 @@ import { base64ToBlob } from "#/utils/base64-to-blob";
import { useUploadFiles } from "../../../hooks/mutation/use-upload-files";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
export const useHandleRuntimeActive = () => {
const dispatch = useDispatch();
@ -29,7 +29,7 @@ export const useHandleRuntimeActive = () => {
{ files: [file] },
{
onError: () => {
toast.error("Failed to upload project files.");
displayErrorToast("Failed to upload project files.");
},
},
);

View File

@ -1,5 +1,4 @@
import React from "react";
import toast from "react-hot-toast";
import { useDispatch } from "react-redux";
import { useWsClient } from "#/context/ws-client-provider";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
@ -7,6 +6,7 @@ import { addErrorMessage } from "#/state/chat-slice";
import { AgentState } from "#/types/agent-state";
import { ErrorObservation } from "#/types/core/observations";
import { useEndSession } from "../../../hooks/use-end-session";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
interface ServerError {
error: boolean | string;
@ -32,15 +32,15 @@ export const useHandleWSEvents = () => {
if (isServerError(event)) {
if (event.error_code === 401) {
toast.error("Session expired.");
displayErrorToast("Session expired.");
endSession();
return;
}
if (typeof event.error === "string") {
toast.error(event.error);
displayErrorToast(event.error);
} else {
toast.error(event.message);
displayErrorToast(event.message);
}
return;
}

View File

@ -3,7 +3,6 @@ import React from "react";
import { Outlet } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { FaServer } from "react-icons/fa";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import {
@ -36,6 +35,7 @@ import { TerminalStatusLabel } from "#/components/features/terminal/terminal-sta
import { useSettings } from "#/hooks/query/use-settings";
import { clearFiles, clearInitialPrompt } from "#/state/initial-query-slice";
import { RootState } from "#/store";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
function AppContent() {
useConversationConfig();
@ -66,7 +66,7 @@ function AppContent() {
React.useEffect(() => {
if (isFetched && !conversation) {
toast.error(
displayErrorToast(
"This conversation does not exist, or you do not have permission to access it.",
);
endSession();

View File

@ -1,25 +0,0 @@
import toast from "react-hot-toast";
export const displayErrorToast = (error: string) => {
toast.error(error, {
position: "top-right",
style: {
background: "#454545",
border: "1px solid #717888",
color: "#fff",
borderRadius: "4px",
},
});
};
export const displaySuccessToast = (message: string) => {
toast.success(message, {
position: "top-right",
style: {
background: "#454545",
border: "1px solid #717888",
color: "#fff",
borderRadius: "4px",
},
});
};

View File

@ -0,0 +1,22 @@
import { CSSProperties } from "react";
import toast, { ToastOptions } from "react-hot-toast";
const TOAST_STYLE: CSSProperties = {
background: "#454545",
border: "1px solid #717888",
color: "#fff",
borderRadius: "4px",
};
const TOAST_OPTIONS: ToastOptions = {
position: "top-right",
style: TOAST_STYLE,
};
export const displayErrorToast = (error: string) => {
toast.error(error, TOAST_OPTIONS);
};
export const displaySuccessToast = (message: string) => {
toast.success(message, TOAST_OPTIONS);
};

View File

@ -1,6 +1,6 @@
import posthog from "posthog-js";
import toast from "react-hot-toast";
import { handleStatusMessage } from "#/services/actions";
import { displayErrorToast } from "./custom-toast-handlers";
interface ErrorDetails {
message: string;
@ -23,7 +23,7 @@ export function showErrorToast({
metadata = {},
}: ErrorDetails) {
trackError({ message, source, metadata });
toast.error(message);
displayErrorToast(message);
}
export function showChatError({

View File

@ -19,6 +19,45 @@ export default {
content: "#ECEDEE", // light gray, used mostly for text
},
},
animation: {
enter: "toastIn 400ms cubic-bezier(0.21, 1.02, 0.73, 1)",
leave: "toastOut 100ms ease-in forwards",
},
keyframes: {
toastIn: {
"0%": {
opacity: "0",
transform: "translateY(-100%) scale(0.8)",
},
"80%": {
opacity: "1",
transform: "translateY(0) scale(1.02)",
},
"100%": {
opacity: "1",
transform: "translateY(0) scale(1)",
},
},
toastOut: {
"0%": {
opacity: "1",
transform: "translateY(0) scale(1)",
},
"100%": {
opacity: "0",
transform: "translateY(-100%) scale(0.9)",
},
},
colors: {
primary: "#C9B974", // nice yellow
base: "#171717", // dark background (neutral-900)
"base-secondary": "#262626", // lighter background (neutral-800); also used for tooltips
danger: "#E76A5E",
success: "#A5E75E",
tertiary: "#454545", // gray, used for inputs
"tertiary-light": "#B7BDC2", // lighter gray, used for borders and placeholder text
},
},
},
darkMode: "class",
plugins: [