mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat: disable settings while task is running (#1291)
* feat: disable settings while task is running * test files * tests * test
This commit is contained in:
parent
a2a86e84f0
commit
620bbb38cf
@ -1,6 +1,7 @@
|
||||
import { render, screen, act } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { vi } from "vitest";
|
||||
import { AutocompleteCombobox } from "./AutocompleteCombobox";
|
||||
|
||||
const onChangeMock = vi.fn();
|
||||
@ -15,6 +16,7 @@ const renderComponent = () =>
|
||||
{ value: "m3", label: "model3" },
|
||||
]}
|
||||
defaultKey="m1"
|
||||
tooltip="tooltip"
|
||||
onChange={onChangeMock}
|
||||
/>,
|
||||
);
|
||||
@ -61,4 +63,6 @@ describe("AutocompleteCombobox", () => {
|
||||
expect(modelInput).toHaveValue("model2");
|
||||
expect(onChangeMock).toHaveBeenCalledWith("model2");
|
||||
});
|
||||
|
||||
it.todo("should show a tooltip after 0.5 seconds of focus");
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Autocomplete, AutocompleteItem } from "@nextui-org/react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { Autocomplete, AutocompleteItem, Tooltip } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
type Label = "model" | "agent" | "language";
|
||||
|
||||
@ -27,7 +27,9 @@ interface AutocompleteComboboxProps {
|
||||
items: AutocompleteItemType[];
|
||||
defaultKey: string;
|
||||
onChange: (key: string) => void;
|
||||
tooltip: string;
|
||||
allowCustomValue?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function AutocompleteCombobox({
|
||||
@ -35,29 +37,43 @@ export function AutocompleteCombobox({
|
||||
items,
|
||||
defaultKey,
|
||||
onChange,
|
||||
tooltip,
|
||||
allowCustomValue = false,
|
||||
disabled = false,
|
||||
}: AutocompleteComboboxProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
aria-label={ariaLabel}
|
||||
label={t(LABELS[ariaLabel])}
|
||||
placeholder={t(PLACEHOLDERS[ariaLabel])}
|
||||
defaultItems={items}
|
||||
defaultSelectedKey={defaultKey}
|
||||
allowsCustomValue={allowCustomValue}
|
||||
onInputChange={(value) => {
|
||||
onChange(value);
|
||||
}}
|
||||
<Tooltip
|
||||
content={
|
||||
disabled
|
||||
? `${tooltip} ${t(I18nKey.SETTINGS$DISABLED_RUNNING)}`
|
||||
: tooltip
|
||||
}
|
||||
closeDelay={100}
|
||||
delay={500}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
<Autocomplete
|
||||
aria-label={ariaLabel}
|
||||
label={t(LABELS[ariaLabel])}
|
||||
placeholder={t(PLACEHOLDERS[ariaLabel])}
|
||||
defaultItems={items}
|
||||
defaultSelectedKey={defaultKey}
|
||||
isDisabled={disabled}
|
||||
allowsCustomValue={allowCustomValue}
|
||||
onInputChange={(value) => {
|
||||
onChange(value);
|
||||
}}
|
||||
>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>
|
||||
)}
|
||||
</Autocomplete>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
AutocompleteCombobox.defaultProps = {
|
||||
allowCustomValue: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import React from "react";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { act, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import AgentTaskState from "#/types/AgentTaskState";
|
||||
import SettingsForm from "./SettingsForm";
|
||||
|
||||
const onModelChangeMock = vi.fn();
|
||||
@ -8,7 +10,7 @@ const onAgentChangeMock = vi.fn();
|
||||
const onLanguageChangeMock = vi.fn();
|
||||
|
||||
const renderSettingsForm = (settings: Partial<Settings>) => {
|
||||
render(
|
||||
renderWithProviders(
|
||||
<SettingsForm
|
||||
settings={settings}
|
||||
models={["model1", "model2", "model3"]}
|
||||
@ -49,6 +51,27 @@ describe("SettingsForm", () => {
|
||||
expect(languageInput).toHaveValue("Español");
|
||||
});
|
||||
|
||||
it("should disable settings while task is running", () => {
|
||||
renderWithProviders(
|
||||
<SettingsForm
|
||||
settings={{}}
|
||||
models={["model1", "model2", "model3"]}
|
||||
agents={["agent1", "agent2", "agent3"]}
|
||||
onModelChange={onModelChangeMock}
|
||||
onAgentChange={onAgentChangeMock}
|
||||
onLanguageChange={onLanguageChangeMock}
|
||||
/>,
|
||||
{ preloadedState: { agent: { curTaskState: AgentTaskState.RUNNING } } },
|
||||
);
|
||||
const modelInput = screen.getByRole("combobox", { name: "model" });
|
||||
const agentInput = screen.getByRole("combobox", { name: "agent" });
|
||||
const languageInput = screen.getByRole("combobox", { name: "language" });
|
||||
|
||||
expect(modelInput).toBeDisabled();
|
||||
expect(agentInput).toBeDisabled();
|
||||
expect(languageInput).toBeDisabled();
|
||||
});
|
||||
|
||||
describe("onChange handlers", () => {
|
||||
it("should call the onModelChange handler when the model changes", () => {
|
||||
renderSettingsForm({});
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import React from "react";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AvailableLanguages } from "../../../i18n";
|
||||
import { I18nKey } from "../../../i18n/declaration";
|
||||
import { RootState } from "../../../store";
|
||||
import AgentTaskState from "../../../types/AgentTaskState";
|
||||
import { AutocompleteCombobox } from "./AutocompleteCombobox";
|
||||
|
||||
interface SettingsFormProps {
|
||||
@ -20,6 +25,21 @@ function SettingsForm({
|
||||
onAgentChange,
|
||||
onLanguageChange,
|
||||
}: SettingsFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const { curTaskState } = useSelector((state: RootState) => state.agent);
|
||||
const [disabled, setDisabled] = React.useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
curTaskState === AgentTaskState.RUNNING ||
|
||||
curTaskState === AgentTaskState.PAUSED
|
||||
) {
|
||||
setDisabled(true);
|
||||
} else {
|
||||
setDisabled(false);
|
||||
}
|
||||
}, [curTaskState, setDisabled]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutocompleteCombobox
|
||||
@ -27,19 +47,25 @@ function SettingsForm({
|
||||
items={models.map((model) => ({ value: model, label: model }))}
|
||||
defaultKey={settings.LLM_MODEL || models[0]}
|
||||
onChange={onModelChange}
|
||||
tooltip={t(I18nKey.SETTINGS$MODEL_TOOLTIP)}
|
||||
allowCustomValue // user can type in a custom LLM model that is not in the list
|
||||
disabled={disabled}
|
||||
/>
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="agent"
|
||||
items={agents.map((agent) => ({ value: agent, label: agent }))}
|
||||
defaultKey={settings.AGENT || agents[0]}
|
||||
onChange={onAgentChange}
|
||||
tooltip={t(I18nKey.SETTINGS$AGENT_TOOLTIP)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<AutocompleteCombobox
|
||||
ariaLabel="language"
|
||||
items={AvailableLanguages}
|
||||
defaultKey={settings.LANGUAGE || "en"}
|
||||
onChange={onLanguageChange}
|
||||
tooltip={t(I18nKey.SETTINGS$LANGUAGE_TOOLTIP)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { waitFor, screen, act, render } from "@testing-library/react";
|
||||
import {
|
||||
fetchAgents,
|
||||
fetchModels,
|
||||
getCurrentSettings,
|
||||
saveSettings,
|
||||
} from "#/services/settingsService";
|
||||
import { act, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { Mock } from "vitest";
|
||||
import {
|
||||
fetchModels,
|
||||
fetchAgents,
|
||||
saveSettings,
|
||||
getCurrentSettings,
|
||||
} from "../../../services/settingsService";
|
||||
import SettingsModal from "./SettingsModal";
|
||||
|
||||
vi.mock("../../services/settingsService", async (importOriginal) => ({
|
||||
@ -28,7 +29,7 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
|
||||
it("should fetch existing agents and models from the API", async () => {
|
||||
render(<SettingsModal isOpen onOpenChange={vi.fn()} />);
|
||||
renderWithProviders(<SettingsModal isOpen onOpenChange={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchModels).toHaveBeenCalledTimes(1);
|
||||
@ -43,7 +44,7 @@ describe("SettingsModal", () => {
|
||||
it("should close the modal when the cancel button is clicked", async () => {
|
||||
const onOpenChange = vi.fn();
|
||||
await act(async () =>
|
||||
render(<SettingsModal isOpen onOpenChange={onOpenChange} />),
|
||||
renderWithProviders(<SettingsModal isOpen onOpenChange={onOpenChange} />),
|
||||
);
|
||||
|
||||
const cancelButton = screen.getByRole("button", {
|
||||
@ -60,7 +61,9 @@ describe("SettingsModal", () => {
|
||||
it("should call saveSettings (and close) with the new values", async () => {
|
||||
const onOpenChangeMock = vi.fn();
|
||||
await act(async () =>
|
||||
render(<SettingsModal isOpen onOpenChange={onOpenChangeMock} />),
|
||||
renderWithProviders(
|
||||
<SettingsModal isOpen onOpenChange={onOpenChangeMock} />,
|
||||
),
|
||||
);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: /save/i });
|
||||
@ -97,7 +100,7 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
|
||||
const onOpenChange = vi.fn();
|
||||
const { rerender } = render(
|
||||
const { rerender } = renderWithProviders(
|
||||
<SettingsModal isOpen onOpenChange={onOpenChange} />,
|
||||
);
|
||||
|
||||
|
||||
@ -308,5 +308,21 @@
|
||||
"ar": "مساعد",
|
||||
"fr": "Assistant",
|
||||
"tr": "Gönder"
|
||||
},
|
||||
"SETTINGS$MODEL_TOOLTIP": {
|
||||
"en": "Select the language model to use.",
|
||||
"de": "Wähle das zu verwendende Model."
|
||||
},
|
||||
"SETTINGS$AGENT_TOOLTIP": {
|
||||
"en": "Select the agent to use.",
|
||||
"de": "Wähle den zu verwendenden Agent."
|
||||
},
|
||||
"SETTINGS$LANGUAGE_TOOLTIP": {
|
||||
"en": "Select the language for the UI.",
|
||||
"de": "Wähle die Sprache für das Interface."
|
||||
},
|
||||
"SETTINGS$DISABLED_RUNNING": {
|
||||
"en": "Cannot be changed while the agent is running.",
|
||||
"de": "Kann nicht geändert werden während ein Task ausgeführt wird."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user