Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): Settings screen #6550

Draft
wants to merge 50 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
4a3b85e
Setup tests and account settings
amanape Jan 30, 2025
7ff53df
Setup setting route
amanape Jan 30, 2025
0bc2955
Styles
amanape Jan 30, 2025
ce06d5c
LLM settings
amanape Jan 30, 2025
ede7fd4
Merge branch 'main' into ALL-1173/settings-screen
amanape Jan 30, 2025
264436d
Advanced toggle
amanape Jan 30, 2025
adf3064
Merge branch 'main' into ALL-1173/settings-screen
amanape Jan 31, 2025
efd28cc
Advanced LLM settings
amanape Jan 31, 2025
027b489
Heading
amanape Jan 31, 2025
ffe5d48
Extend test
amanape Jan 31, 2025
7ee9c8c
Style switch components
amanape Jan 31, 2025
077adf3
Fix small nits
amanape Jan 31, 2025
e56b7b2
Refactor
amanape Jan 31, 2025
667de99
Small style nit
amanape Jan 31, 2025
4cbaf7a
Settings dropdown
amanape Jan 31, 2025
588d8f9
Refactor
amanape Jan 31, 2025
7afb45c
Merge branch 'main' into ALL-1173/settings-screen
amanape Feb 3, 2025
dc1fdde
Confirmation mode
amanape Feb 3, 2025
a63113c
Improved dropdown styles
amanape Feb 3, 2025
fd9d8bb
Placeholder and styles
amanape Feb 3, 2025
cbf4f0a
Update dropdown tests and logic
amanape Feb 3, 2025
2b81bff
Advanced settings logic
amanape Feb 3, 2025
b3b7600
Load existing settings
amanape Feb 3, 2025
c239d7e
Save settings
amanape Feb 3, 2025
430ae36
Toasts
amanape Feb 3, 2025
c339b24
Fix url
amanape Feb 3, 2025
630ab1f
Merge branch 'main' into ALL-1173/settings-screen
amanape Feb 4, 2025
2c368de
No data loaders
amanape Feb 4, 2025
95aa972
Fix test
amanape Feb 4, 2025
48b9d4c
Finally fix tests
amanape Feb 4, 2025
3325093
AI settings
amanape Feb 4, 2025
fc65f95
Model selector
amanape Feb 4, 2025
c8f3cd2
Merge branch 'main' into ALL-1173/settings-screen
amanape Feb 5, 2025
5ce0f75
Reset logic
amanape Feb 5, 2025
b163bd8
Handle analytics consent
amanape Feb 5, 2025
34906f7
Merge branch 'main' into ALL-1173/settings-screen
amanape Feb 6, 2025
0441236
Sidebar styles
amanape Feb 6, 2025
c166d21
Small refactor
amanape Feb 6, 2025
148e461
Reset modal
amanape Feb 6, 2025
1dcb9c5
Merge
amanape Feb 6, 2025
9751da6
Nav settings
amanape Feb 7, 2025
39a22fe
Fix tests
amanape Feb 7, 2025
be39f4c
Move to settings when clicking connect to github
amanape Feb 7, 2025
11b6c1f
Conditional renders given whether user is signed in or key is set
amanape Feb 7, 2025
54b6343
No settings default
amanape Feb 7, 2025
7f59fdb
Handle small errors
amanape Feb 7, 2025
528214b
Small style fix
amanape Feb 7, 2025
fe3fea8
Test and cover edge cases
amanape Feb 7, 2025
9dde165
Replace with NextUI dropdown
amanape Feb 7, 2025
7e052d4
Style fixes
amanape Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ describe("AccountSettingsContextMenu", () => {
it("should always render the right options", () => {
render(
<AccountSettingsContextMenu
onClickAccountSettings={onClickAccountSettingsMock}
onLogout={onLogoutMock}
onClose={onCloseMock}
isLoggedIn
Expand All @@ -28,30 +27,12 @@ describe("AccountSettingsContextMenu", () => {
expect(
screen.getByTestId("account-settings-context-menu"),
).toBeInTheDocument();
expect(screen.getByText("ACCOUNT_SETTINGS$SETTINGS")).toBeInTheDocument();
expect(screen.getByText("ACCOUNT_SETTINGS$LOGOUT")).toBeInTheDocument();
});

it("should call onClickAccountSettings when the account settings option is clicked", async () => {
render(
<AccountSettingsContextMenu
onClickAccountSettings={onClickAccountSettingsMock}
onLogout={onLogoutMock}
onClose={onCloseMock}
isLoggedIn
/>,
);

const accountSettingsOption = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
await user.click(accountSettingsOption);

expect(onClickAccountSettingsMock).toHaveBeenCalledOnce();
});

it("should call onLogout when the logout option is clicked", async () => {
render(
<AccountSettingsContextMenu
onClickAccountSettings={onClickAccountSettingsMock}
onLogout={onLogoutMock}
onClose={onCloseMock}
isLoggedIn
Expand All @@ -67,7 +48,6 @@ describe("AccountSettingsContextMenu", () => {
test("onLogout should be disabled if the user is not logged in", async () => {
render(
<AccountSettingsContextMenu
onClickAccountSettings={onClickAccountSettingsMock}
onLogout={onLogoutMock}
onClose={onCloseMock}
isLoggedIn={false}
Expand All @@ -83,14 +63,13 @@ describe("AccountSettingsContextMenu", () => {
it("should call onClose when clicking outside of the element", async () => {
render(
<AccountSettingsContextMenu
onClickAccountSettings={onClickAccountSettingsMock}
onLogout={onLogoutMock}
onClose={onCloseMock}
isLoggedIn
/>,
);

const accountSettingsButton = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
const accountSettingsButton = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
await user.click(accountSettingsButton);
await user.click(document.body);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SettingsProvider } from "#/context/settings-context";
import { AuthProvider } from "#/context/auth-context";

describe("AnalyticsConsentFormModal", () => {
it("should call saveUserSettings with default settings on confirm reset settings", async () => {
it("should call saveUserSettings with consent", async () => {
const user = userEvent.setup();
const onCloseMock = vi.fn();
const saveUserSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
Expand All @@ -26,20 +26,9 @@ describe("AnalyticsConsentFormModal", () => {
const confirmButton = screen.getByTestId("confirm-preferences");
await user.click(confirmButton);

expect(saveUserSettingsSpy).toHaveBeenCalledWith({
user_consents_to_analytics: true,
agent: "CodeActAgent",
confirmation_mode: false,
enable_default_condenser: false,
github_token: undefined,
language: "en",
llm_api_key: undefined,
llm_base_url: "",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
security_analyzer: "",
unset_github_token: undefined,
});
expect(saveUserSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({ user_consents_to_analytics: true }),
);
expect(onCloseMock).toHaveBeenCalled();
});
});
162 changes: 6 additions & 156 deletions frontend/__tests__/components/features/sidebar/sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import { AxiosError } from "axios";
import { Sidebar } from "#/components/features/sidebar/sidebar";
import OpenHands from "#/api/open-hands";

Expand All @@ -21,161 +18,14 @@ const renderSidebar = () =>
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);

describe("Sidebar", () => {
describe("Settings", () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");

afterEach(() => {
vi.clearAllMocks();
});

it("should fetch settings data on mount", () => {
renderSidebar();
expect(getSettingsSpy).toHaveBeenCalledOnce();
});

it("should send all settings data when saving AI configuration", async () => {
const user = userEvent.setup();
renderSidebar();

const settingsButton = screen.getByTestId("settings-button");
await user.click(settingsButton);

const settingsModal = screen.getByTestId("ai-config-modal");
const saveButton = within(settingsModal).getByTestId(
"save-settings-button",
);
await user.click(saveButton);

expect(saveSettingsSpy).toHaveBeenCalledWith({
agent: "CodeActAgent",
confirmation_mode: false,
enable_default_condenser: false,
language: "en",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
});
});

it("should not reset AI configuration when saving account settings", async () => {
const user = userEvent.setup();
renderSidebar();

const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);

const menu = screen.getByTestId("account-settings-context-menu");
const accountSettingsButton = within(menu).getByTestId(
"account-settings-button",
);
await user.click(accountSettingsButton);

const accountSettingsModal = screen.getByTestId("account-settings-form");

const languageInput =
within(accountSettingsModal).getByLabelText(/language/i);
await user.click(languageInput);

const norskOption = screen.getByText(/norsk/i);
await user.click(norskOption);

const tokenInput =
within(accountSettingsModal).getByLabelText(/GITHUB\$TOKEN_LABEL/i);
await user.type(tokenInput, "new-token");

const analyticsConsentInput =
within(accountSettingsModal).getByTestId("analytics-consent");
await user.click(analyticsConsentInput);

const saveButton =
within(accountSettingsModal).getByTestId("save-settings");
await user.click(saveButton);

expect(saveSettingsSpy).toHaveBeenCalledWith({
agent: "CodeActAgent",
confirmation_mode: false,
enable_default_condenser: false,
github_token: "new-token",
language: "no",
llm_base_url: "",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
security_analyzer: "",
user_consents_to_analytics: true,
});
});

it("should not send the api key if its SET", async () => {
const user = userEvent.setup();
renderSidebar();

const settingsButton = screen.getByTestId("settings-button");
await user.click(settingsButton);

const settingsModal = screen.getByTestId("ai-config-modal");

// Click the advanced options switch to show the API key input
const advancedOptionsSwitch = within(settingsModal).getByTestId(
"advanced-option-switch",
);
await user.click(advancedOptionsSwitch);

const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i);
await user.type(apiKeyInput, "**********");

const saveButton = within(settingsModal).getByTestId(
"save-settings-button",
);
await user.click(saveButton);

expect(saveSettingsSpy).toHaveBeenCalledWith({
agent: "CodeActAgent",
confirmation_mode: false,
enable_default_condenser: false,
language: "en",
llm_base_url: "",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
});
});
afterEach(() => {
vi.clearAllMocks();
});

describe("Settings Modal", () => {
it("should open the settings modal if the user clicks the settings button", async () => {
const user = userEvent.setup();
renderSidebar();

expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument();

const settingsButton = screen.getByTestId("settings-button");
await user.click(settingsButton);

const settingsModal = screen.getByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
});

it("should open the settings modal if GET /settings fails with a 404", async () => {
const error = new AxiosError(
"Request failed with status code 404",
"ERR_BAD_REQUEST",
undefined,
undefined,
{
status: 404,
statusText: "Not Found",
data: { message: "Settings not found" },
headers: {},
// @ts-expect-error - we only need the response object for this test
config: {},
},
);

vi.spyOn(OpenHands, "getSettings").mockRejectedValue(error);

renderSidebar();

const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
});
it("should fetch settings data on mount", () => {
renderSidebar();
expect(getSettingsSpy).toHaveBeenCalledOnce();
});
});
Loading