refactor(frontend): migration of metrics-slice.ts to zustand (#11018)

This commit is contained in:
Hiep Le 2025-09-19 23:52:21 +07:00 committed by GitHub
parent e09f93aa75
commit b89f2e51e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 58 additions and 151 deletions

View File

@ -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(<RouterStub />, {
preloadedState: {
metrics: {
cost: null,
max_budget_per_task: null,
usage: null,
},
},
});
const renderConversationPanel = () => renderWithQueryAndI18n(<RouterStub />);
beforeAll(() => {
vi.mock("react-router", async (importOriginal) => ({
@ -297,15 +287,7 @@ describe("ConversationPanel", () => {
},
]);
renderWithProviders(<MyRouterStub />, {
preloadedState: {
metrics: {
cost: null,
max_budget_per_task: null,
usage: null,
},
},
});
renderWithQueryAndI18n(<MyRouterStub />);
const toggleButton = screen.getByText("Toggle");

View File

@ -57,11 +57,6 @@ describe("MicroagentManagement", () => {
const renderMicroagentManagement = (config?: QueryClientConfig) =>
renderWithProviders(<RouterStub />, {
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(<RouterStub />, {
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(<MicroagentManagementMain />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
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(<RouterStub />, {
preloadedState: {
metrics: {
cost: null,
max_budget_per_task: null,
usage: null,
},
microagentManagement: {
selectedMicroagentItem: {
microagent: mockMicroagentForLearn,

View File

@ -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 (
<BaseModal

View File

@ -2,10 +2,9 @@ import { useTranslation } from "react-i18next";
import React from "react";
import posthog from "posthog-js";
import { useParams, useNavigate } from "react-router";
import { useSelector } from "react-redux";
import { useWsClient } from "#/context/ws-client-provider";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import { RootState } from "#/store";
import useMetricsStore from "#/stores/metrics-store";
import { isSystemMessage } from "#/types/core/guards";
import { ConversationStatus } from "#/types/conversation-status";
import ConversationService from "#/api/conversation-service/conversation-service.api";
@ -36,7 +35,7 @@ export function useConversationNameContextMenu({
const { mutate: deleteConversation } = useDeleteConversation();
const { mutate: stopConversation } = useStopConversation();
const { mutate: getTrajectory } = useGetTrajectory();
const metrics = useSelector((state: RootState) => state.metrics);
const metrics = useMetricsStore();
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
const [systemModalVisible, setSystemModalVisible] = React.useState(false);

View File

@ -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) {

View File

@ -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<MetricsState>) => {
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;

View File

@ -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,

View File

@ -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<MetricsStore>((set) => ({
cost: null,
max_budget_per_task: null,
usage: null,
setMetrics: (metrics) => set(metrics),
}));
export default useMetricsStore;

View File

@ -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<RenderOptions, "wrapper"> = {},
) {
function Wrapper({ children }: PropsWithChildren) {
return (
<QueryClientProvider
client={
new QueryClient({
defaultOptions: { queries: { retry: false } },
})
}
>
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
</QueryClientProvider>
);
}
return render(ui, { wrapper: Wrapper, ...renderOptions });
}
export const createAxiosNotFoundErrorObject = () =>
new AxiosError(
"Request failed with status code 404",