From b89f2e51e45c00792a18384d067a30b2f1b8ceda Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Fri, 19 Sep 2025 23:52:21 +0700 Subject: [PATCH] refactor(frontend): migration of metrics-slice.ts to zustand (#11018) --- .../conversation-panel.test.tsx | 24 +----- .../microagent-management.test.tsx | 85 ------------------- .../metrics-modal/metrics-modal.tsx | 5 +- .../use-conversation-name-context-menu.ts | 5 +- frontend/src/services/actions.ts | 4 +- frontend/src/state/metrics-slice.ts | 35 -------- frontend/src/store.ts | 2 - frontend/src/stores/metrics-store.ts | 27 ++++++ frontend/test-utils.tsx | 22 +++++ 9 files changed, 58 insertions(+), 151 deletions(-) delete mode 100644 frontend/src/state/metrics-slice.ts create mode 100644 frontend/src/stores/metrics-store.ts diff --git a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx index 9ea5c0fefa..7df58fab97 100644 --- a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx +++ b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx @@ -1,10 +1,9 @@ import { screen, waitFor, within } from "@testing-library/react"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { QueryClientConfig } from "@tanstack/react-query"; import userEvent from "@testing-library/user-event"; import { createRoutesStub } from "react-router"; import React from "react"; -import { renderWithProviders } from "test-utils"; +import { renderWithQueryAndI18n } from "test-utils"; import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel"; import ConversationService from "#/api/conversation-service/conversation-service.api"; import { Conversation } from "#/api/open-hands.types"; @@ -18,16 +17,7 @@ describe("ConversationPanel", () => { }, ]); - const renderConversationPanel = (config?: QueryClientConfig) => - renderWithProviders(, { - preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, - }, - }); + const renderConversationPanel = () => renderWithQueryAndI18n(); beforeAll(() => { vi.mock("react-router", async (importOriginal) => ({ @@ -297,15 +287,7 @@ describe("ConversationPanel", () => { }, ]); - renderWithProviders(, { - preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, - }, - }); + renderWithQueryAndI18n(); const toggleButton = screen.getByText("Toggle"); diff --git a/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx b/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx index 719b809f74..c0f6207b43 100644 --- a/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx +++ b/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx @@ -57,11 +57,6 @@ describe("MicroagentManagement", () => { const renderMicroagentManagement = (config?: QueryClientConfig) => renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { addMicroagentModalVisible: false, updateMicroagentModalVisible: false, @@ -1351,11 +1346,6 @@ describe("MicroagentManagement", () => { // Render with modal already visible in Redux state renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: null, addMicroagentModalVisible: true, // Start with modal visible @@ -1646,11 +1636,6 @@ describe("MicroagentManagement", () => { const renderMicroagentManagementMain = (selectedMicroagentItem: any) => renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { addMicroagentModalVisible: false, selectedRepository: { @@ -1998,11 +1983,6 @@ describe("MicroagentManagement", () => { // Render with update modal visible in Redux state renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForUpdate, @@ -2037,11 +2017,6 @@ describe("MicroagentManagement", () => { // Render with update modal visible and selected microagent renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForUpdate, @@ -2075,11 +2050,6 @@ describe("MicroagentManagement", () => { // Render with update modal visible and selected microagent renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForUpdate, @@ -2118,11 +2088,6 @@ describe("MicroagentManagement", () => { // Render with update modal visible and selected microagent renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForUpdate, @@ -2174,11 +2139,6 @@ describe("MicroagentManagement", () => { // Render with update modal visible renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForUpdate, @@ -2225,11 +2185,6 @@ describe("MicroagentManagement", () => { // Render with update modal visible renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForUpdate, @@ -2279,11 +2234,6 @@ describe("MicroagentManagement", () => { // Render with update modal visible but no microagent data renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: null, addMicroagentModalVisible: false, @@ -2325,11 +2275,6 @@ describe("MicroagentManagement", () => { // Render with update modal visible and microagent renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForUpdate, @@ -2374,11 +2319,6 @@ describe("MicroagentManagement", () => { // Render with update modal visible and microagent renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForUpdate, @@ -2561,11 +2501,6 @@ describe("MicroagentManagement", () => { // Render with selected microagent renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForLearn, @@ -2601,11 +2536,6 @@ describe("MicroagentManagement", () => { // Render with selected microagent renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForLearn, @@ -2658,11 +2588,6 @@ describe("MicroagentManagement", () => { // Render with selected microagent renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForLearn, @@ -2718,11 +2643,6 @@ describe("MicroagentManagement", () => { // Render with selected microagent renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForLearn, @@ -2776,11 +2696,6 @@ describe("MicroagentManagement", () => { // Render with selected microagent renderWithProviders(, { preloadedState: { - metrics: { - cost: null, - max_budget_per_task: null, - usage: null, - }, microagentManagement: { selectedMicroagentItem: { microagent: mockMicroagentForLearn, diff --git a/frontend/src/components/features/conversation/metrics-modal/metrics-modal.tsx b/frontend/src/components/features/conversation/metrics-modal/metrics-modal.tsx index e055bb5c6b..af6e314637 100644 --- a/frontend/src/components/features/conversation/metrics-modal/metrics-modal.tsx +++ b/frontend/src/components/features/conversation/metrics-modal/metrics-modal.tsx @@ -1,12 +1,11 @@ import { useTranslation } from "react-i18next"; -import { useSelector } from "react-redux"; import { BaseModal } from "../../../shared/modals/base-modal/base-modal"; -import { RootState } from "#/store"; import { I18nKey } from "#/i18n/declaration"; import { CostSection } from "./cost-section"; import { UsageSection } from "./usage-section"; import { ContextWindowSection } from "./context-window-section"; import { EmptyState } from "./empty-state"; +import useMetricsStore from "#/stores/metrics-store"; interface MetricsModalProps { isOpen: boolean; @@ -15,7 +14,7 @@ interface MetricsModalProps { export function MetricsModal({ isOpen, onOpenChange }: MetricsModalProps) { const { t } = useTranslation(); - const metrics = useSelector((state: RootState) => state.metrics); + const metrics = useMetricsStore(); return ( state.metrics); + const metrics = useMetricsStore(); const [metricsModalVisible, setMetricsModalVisible] = React.useState(false); const [systemModalVisible, setSystemModalVisible] = React.useState(false); diff --git a/frontend/src/services/actions.ts b/frontend/src/services/actions.ts index 9cbfa34e3f..53fed792a7 100644 --- a/frontend/src/services/actions.ts +++ b/frontend/src/services/actions.ts @@ -1,7 +1,7 @@ import { trackError } from "#/utils/error-handler"; import { appendSecurityAnalyzerInput } from "#/state/security-analyzer-slice"; +import useMetricsStore from "#/stores/metrics-store"; import { useStatusStore } from "#/state/status-store"; -import { setMetrics } from "#/state/metrics-slice"; import store from "#/store"; import ActionType from "#/types/action-type"; import { @@ -26,7 +26,7 @@ export function handleActionMessage(message: ActionMessage) { max_budget_per_task: message.llm_metrics?.max_budget_per_task ?? null, usage: message.llm_metrics?.accumulated_token_usage ?? null, }; - store.dispatch(setMetrics(metrics)); + useMetricsStore.getState().setMetrics(metrics); } if (message.action === ActionType.RUN) { diff --git a/frontend/src/state/metrics-slice.ts b/frontend/src/state/metrics-slice.ts deleted file mode 100644 index 77154340b1..0000000000 --- a/frontend/src/state/metrics-slice.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit"; - -interface MetricsState { - cost: number | null; - max_budget_per_task: number | null; - usage: { - prompt_tokens: number; - completion_tokens: number; - cache_read_tokens: number; - cache_write_tokens: number; - context_window: number; - per_turn_token: number; - } | null; -} - -const initialState: MetricsState = { - cost: null, - max_budget_per_task: null, - usage: null, -}; - -const metricsSlice = createSlice({ - name: "metrics", - initialState, - reducers: { - setMetrics: (state, action: PayloadAction) => { - state.cost = action.payload.cost; - state.max_budget_per_task = action.payload.max_budget_per_task; - state.usage = action.payload.usage; - }, - }, -}); - -export const { setMetrics } = metricsSlice.actions; -export default metricsSlice.reducer; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 8c3b6813a3..8da8d3734d 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -4,7 +4,6 @@ import browserReducer from "./state/browser-slice"; import fileStateReducer from "./state/file-state-slice"; import { jupyterReducer } from "./state/jupyter-slice"; import securityAnalyzerReducer from "./state/security-analyzer-slice"; -import metricsReducer from "./state/metrics-slice"; import microagentManagementReducer from "./state/microagent-management-slice"; import conversationReducer from "./state/conversation-slice"; import eventMessageReducer from "./state/event-message-slice"; @@ -15,7 +14,6 @@ export const rootReducer = combineReducers({ agent: agentReducer, jupyter: jupyterReducer, securityAnalyzer: securityAnalyzerReducer, - metrics: metricsReducer, microagentManagement: microagentManagementReducer, conversation: conversationReducer, eventMessage: eventMessageReducer, diff --git a/frontend/src/stores/metrics-store.ts b/frontend/src/stores/metrics-store.ts new file mode 100644 index 0000000000..82060e28c4 --- /dev/null +++ b/frontend/src/stores/metrics-store.ts @@ -0,0 +1,27 @@ +import { create } from "zustand"; + +interface MetricsState { + cost: number | null; + max_budget_per_task: number | null; + usage: { + prompt_tokens: number; + completion_tokens: number; + cache_read_tokens: number; + cache_write_tokens: number; + context_window: number; + per_turn_token: number; + } | null; +} + +interface MetricsStore extends MetricsState { + setMetrics: (metrics: MetricsState) => void; +} + +const useMetricsStore = create((set) => ({ + cost: null, + max_budget_per_task: null, + usage: null, + setMetrics: (metrics) => set(metrics), +})); + +export default useMetricsStore; diff --git a/frontend/test-utils.tsx b/frontend/test-utils.tsx index e5ddd89fc1..c882a054d8 100644 --- a/frontend/test-utils.tsx +++ b/frontend/test-utils.tsx @@ -79,6 +79,28 @@ export function renderWithProviders( return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }; } +// Export a render function for components that only need QueryClient and i18next providers +// (without Redux store) +export function renderWithQueryAndI18n( + ui: React.ReactElement, + renderOptions: Omit = {}, +) { + function Wrapper({ children }: PropsWithChildren) { + return ( + + {children} + + ); + } + return render(ui, { wrapper: Wrapper, ...renderOptions }); +} + export const createAxiosNotFoundErrorObject = () => new AxiosError( "Request failed with status code 404",