mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
refactor(frontend): Auth (#8308)
This commit is contained in:
parent
ac0dab41dd
commit
d3f6508e32
@ -1,8 +1,9 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ActionSuggestions } from "#/components/features/chat/action-suggestions";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useSelector } from "react-redux";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("posthog-js", () => ({
|
||||
@ -11,8 +12,12 @@ vi.mock("posthog-js", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const { useSelectorMock } = vi.hoisted(() => ({
|
||||
useSelectorMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("react-redux", () => ({
|
||||
useSelector: vi.fn(),
|
||||
useSelector: useSelectorMock,
|
||||
}));
|
||||
|
||||
vi.mock("#/context/auth-context", () => ({
|
||||
@ -24,34 +29,46 @@ vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"ACTION$PUSH_TO_BRANCH": "Push to Branch",
|
||||
"ACTION$PUSH_CREATE_PR": "Push & Create PR",
|
||||
"ACTION$PUSH_CHANGES_TO_PR": "Push Changes to PR"
|
||||
ACTION$PUSH_TO_BRANCH: "Push to Branch",
|
||||
ACTION$PUSH_CREATE_PR: "Push & Create PR",
|
||||
ACTION$PUSH_CHANGES_TO_PR: "Push Changes to PR",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderActionSuggestions = () =>
|
||||
render(<ActionSuggestions onSuggestionsClick={() => {}} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
describe("ActionSuggestions", () => {
|
||||
// Setup mocks for each test
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
(useAuth as any).mockReturnValue({
|
||||
providersAreSet: true,
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: "some-token",
|
||||
},
|
||||
});
|
||||
|
||||
(useSelector as any).mockReturnValue({
|
||||
useSelectorMock.mockReturnValue({
|
||||
selectedRepository: "test-repo",
|
||||
});
|
||||
});
|
||||
|
||||
it("should render both GitHub buttons when GitHub token is set and repository is selected", () => {
|
||||
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
|
||||
it("should render both GitHub buttons when GitHub token is set and repository is selected", async () => {
|
||||
renderActionSuggestions();
|
||||
|
||||
// Find all buttons with data-testid="suggestion"
|
||||
const buttons = screen.getAllByTestId("suggestion");
|
||||
const buttons = await screen.findAllByTestId("suggestion");
|
||||
|
||||
// Check if we have at least 2 buttons
|
||||
expect(buttons.length).toBeGreaterThanOrEqual(2);
|
||||
@ -69,30 +86,24 @@ describe("ActionSuggestions", () => {
|
||||
});
|
||||
|
||||
it("should not render buttons when GitHub token is not set", () => {
|
||||
(useAuth as any).mockReturnValue({
|
||||
providersAreSet: false,
|
||||
});
|
||||
|
||||
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
|
||||
renderActionSuggestions();
|
||||
|
||||
expect(screen.queryByTestId("suggestion")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render buttons when no repository is selected", () => {
|
||||
(useSelector as any).mockReturnValue({
|
||||
useSelectorMock.mockReturnValue({
|
||||
selectedRepository: null,
|
||||
});
|
||||
|
||||
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
|
||||
renderActionSuggestions();
|
||||
|
||||
expect(screen.queryByTestId("suggestion")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have different prompts for 'Push to Branch' and 'Push & Create PR' buttons", () => {
|
||||
// This test verifies that the prompts are different in the component
|
||||
const component = render(
|
||||
<ActionSuggestions onSuggestionsClick={() => {}} />,
|
||||
);
|
||||
renderActionSuggestions();
|
||||
|
||||
// Get the component instance to access the internal values
|
||||
const pushBranchPrompt =
|
||||
|
||||
@ -4,7 +4,6 @@ import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
|
||||
describe("AnalyticsConsentFormModal", () => {
|
||||
it("should call saveUserSettings with consent", async () => {
|
||||
@ -14,11 +13,9 @@ describe("AnalyticsConsentFormModal", () => {
|
||||
|
||||
render(<AnalyticsConsentFormModal onClose={onCloseMock} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@ -2,22 +2,15 @@ import { render, screen } from "@testing-library/react";
|
||||
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { AuthModal } from "#/components/features/waitlist/auth-modal";
|
||||
import * as AuthHook from "#/context/auth-context";
|
||||
|
||||
// Mock the useAuthUrl hook
|
||||
vi.mock("#/hooks/use-auth-url", () => ({
|
||||
useAuthUrl: () => "https://gitlab.com/oauth/authorize"
|
||||
useAuthUrl: () => "https://gitlab.com/oauth/authorize",
|
||||
}));
|
||||
|
||||
describe("AuthModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("location", { href: "" });
|
||||
vi.spyOn(AuthHook, "useAuth").mockReturnValue({
|
||||
providersAreSet: false,
|
||||
setProvidersAreSet: vi.fn(),
|
||||
providerTokensSet: [],
|
||||
setProviderTokensSet: vi.fn()
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -28,8 +21,12 @@ describe("AuthModal", () => {
|
||||
it("should render the GitHub and GitLab buttons", () => {
|
||||
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
|
||||
|
||||
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" });
|
||||
const githubButton = screen.getByRole("button", {
|
||||
name: "GITHUB$CONNECT_TO_GITHUB",
|
||||
});
|
||||
const gitlabButton = screen.getByRole("button", {
|
||||
name: "GITLAB$CONNECT_TO_GITLAB",
|
||||
});
|
||||
|
||||
expect(githubButton).toBeInTheDocument();
|
||||
expect(gitlabButton).toBeInTheDocument();
|
||||
@ -40,7 +37,9 @@ describe("AuthModal", () => {
|
||||
const mockUrl = "https://github.com/login/oauth/authorize";
|
||||
render(<AuthModal githubAuthUrl={mockUrl} appMode="saas" />);
|
||||
|
||||
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
const githubButton = screen.getByRole("button", {
|
||||
name: "GITHUB$CONNECT_TO_GITHUB",
|
||||
});
|
||||
await user.click(githubButton);
|
||||
|
||||
expect(window.location.href).toBe(mockUrl);
|
||||
|
||||
@ -1,19 +1,13 @@
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
QueryClientProvider,
|
||||
QueryClient,
|
||||
QueryClientConfig,
|
||||
} from "@tanstack/react-query";
|
||||
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 { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { clickOnEditButton } from "./utils";
|
||||
import { queryClientConfig } from "#/query-client-config";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
|
||||
describe("ConversationPanel", () => {
|
||||
const onCloseMock = vi.fn();
|
||||
@ -29,9 +23,9 @@ describe("ConversationPanel", () => {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
usage: null
|
||||
}
|
||||
}
|
||||
usage: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
@ -75,7 +69,9 @@ describe("ConversationPanel", () => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
// Setup default mock for getUserConversations
|
||||
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([...mockConversations]);
|
||||
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([
|
||||
...mockConversations,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should render the conversations", async () => {
|
||||
@ -129,7 +125,9 @@ describe("ConversationPanel", () => {
|
||||
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(screen.queryByRole("button", { name: /cancel/i })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /cancel/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Ensure the conversation is not deleted
|
||||
cards = await screen.findAllByTestId("conversation-card");
|
||||
@ -168,9 +166,12 @@ describe("ConversationPanel", () => {
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockImplementation(async () => mockData);
|
||||
|
||||
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
|
||||
const deleteUserConversationSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
"deleteUserConversation",
|
||||
);
|
||||
deleteUserConversationSpy.mockImplementation(async (id: string) => {
|
||||
const index = mockData.findIndex(conv => conv.conversation_id === id);
|
||||
const index = mockData.findIndex((conv) => conv.conversation_id === id);
|
||||
if (index !== -1) {
|
||||
mockData.splice(index, 1);
|
||||
}
|
||||
@ -178,7 +179,7 @@ describe("ConversationPanel", () => {
|
||||
|
||||
renderConversationPanel();
|
||||
|
||||
let cards = await screen.findAllByTestId("conversation-card");
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(3);
|
||||
|
||||
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
|
||||
@ -192,7 +193,9 @@ describe("ConversationPanel", () => {
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /confirm/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Wait for the cards to update
|
||||
await waitFor(() => {
|
||||
@ -298,9 +301,9 @@ describe("ConversationPanel", () => {
|
||||
preloadedState: {
|
||||
metrics: {
|
||||
cost: null,
|
||||
usage: null
|
||||
}
|
||||
}
|
||||
usage: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const toggleButton = screen.getByText("Toggle");
|
||||
|
||||
@ -5,7 +5,6 @@ import { createRoutesStub } from "react-router";
|
||||
import { setupStore } from "test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { HomeHeader } from "#/components/features/home/home-header";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
@ -24,11 +23,9 @@ const renderHomeHeader = () => {
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<AuthProvider initialProvidersAreSet>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import { setupStore } from "test-utils";
|
||||
import { Provider } from "react-redux";
|
||||
import { createRoutesStub, Outlet } from "react-router";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { RepoConnector } from "#/components/features/home/repo-connector";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
|
||||
const renderRepoConnector = (initialProvidersAreSet = true) => {
|
||||
const renderRepoConnector = () => {
|
||||
const mockRepoSelection = vi.fn();
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
@ -40,11 +40,9 @@ const renderRepoConnector = (initialProvidersAreSet = true) => {
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<AuthProvider initialProvidersAreSet={initialProvidersAreSet}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
@ -65,6 +63,17 @@ const MOCK_RESPOSITORIES: GitRepository[] = [
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: "some-token",
|
||||
gitlab: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("RepoConnector", () => {
|
||||
it("should render the repository connector section", () => {
|
||||
renderRepoConnector();
|
||||
@ -99,7 +108,7 @@ describe("RepoConnector", () => {
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
const launchButton = screen.getByTestId("repo-launch-button");
|
||||
const launchButton = await screen.findByTestId("repo-launch-button");
|
||||
expect(launchButton).toBeDisabled();
|
||||
|
||||
// Wait for the loading state to be replaced with the dropdown
|
||||
@ -147,7 +156,7 @@ describe("RepoConnector", () => {
|
||||
|
||||
const repoConnector = screen.getByTestId("repo-connector");
|
||||
const launchButton =
|
||||
within(repoConnector).getByTestId("repo-launch-button");
|
||||
await within(repoConnector).findByTestId("repo-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
// repo not selected yet
|
||||
@ -184,7 +193,7 @@ describe("RepoConnector", () => {
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
const launchButton = screen.getByTestId("repo-launch-button");
|
||||
const launchButton = await screen.findByTestId("repo-launch-button");
|
||||
|
||||
// Wait for the loading state to be replaced with the dropdown
|
||||
const dropdown = await waitFor(() => screen.getByTestId("repo-dropdown"));
|
||||
@ -197,14 +206,22 @@ describe("RepoConnector", () => {
|
||||
});
|
||||
|
||||
it("should not display a button to settings if the user is signed in with their git provider", async () => {
|
||||
renderRepoConnector(true);
|
||||
expect(
|
||||
screen.queryByTestId("navigate-to-settings-button"),
|
||||
).not.toBeInTheDocument();
|
||||
renderRepoConnector();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId("navigate-to-settings-button"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display a button to settings if the user needs to sign in with their git provider", async () => {
|
||||
renderRepoConnector(false);
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {},
|
||||
});
|
||||
renderRepoConnector();
|
||||
|
||||
const goToSettingsButton = await screen.findByTestId(
|
||||
"navigate-to-settings-button",
|
||||
|
||||
@ -7,7 +7,6 @@ import { createRoutesStub } from "react-router";
|
||||
import { setupStore } from "test-utils";
|
||||
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { TaskCard } from "#/components/features/home/tasks/task-card";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
@ -41,11 +40,9 @@ const renderTaskCard = (task = MOCK_TASK_1) => {
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<AuthProvider initialProvidersAreSet>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
|
||||
@ -7,9 +7,8 @@ import { setupStore } from "test-utils";
|
||||
import { TaskSuggestions } from "#/components/features/home/tasks/task-suggestions";
|
||||
import { SuggestionsService } from "#/api/suggestions-service/suggestions-service.api";
|
||||
import { MOCK_TASKS } from "#/mocks/task-suggestions-handlers";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
|
||||
const renderTaskSuggestions = (initialProvidersAreSet = true) => {
|
||||
const renderTaskSuggestions = () => {
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: TaskSuggestions,
|
||||
@ -28,11 +27,9 @@ const renderTaskSuggestions = (initialProvidersAreSet = true) => {
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<AuthProvider initialProvidersAreSet={initialProvidersAreSet}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
@ -85,7 +82,7 @@ describe("TaskSuggestions", () => {
|
||||
getSuggestedTasksSpy.mockResolvedValue(MOCK_TASKS);
|
||||
renderTaskSuggestions();
|
||||
|
||||
const skeletons = screen.getAllByTestId("task-group-skeleton");
|
||||
const skeletons = await screen.findAllByTestId("task-group-skeleton");
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
import { Sidebar } from "#/components/features/sidebar/sidebar";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
@ -24,8 +25,8 @@ describe("Sidebar", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should fetch settings data on mount", () => {
|
||||
it("should fetch settings data on mount", async () => {
|
||||
renderSidebar();
|
||||
expect(getSettingsSpy).toHaveBeenCalled();
|
||||
await waitFor(() => expect(getSettingsSpy).toHaveBeenCalled());
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
WsClientProvider,
|
||||
useWsClient,
|
||||
} from "#/context/ws-client-provider";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
|
||||
describe("Propagate error message", () => {
|
||||
it("should do nothing when no message was passed from server", () => {
|
||||
@ -91,11 +90,9 @@ describe("WsClientProvider", () => {
|
||||
const { getByText } = render(<TestComponent />, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<AuthProvider initialProviderTokens={[]}>
|
||||
<WsClientProvider conversationId="test-conversation-id">
|
||||
{children}
|
||||
</WsClientProvider>
|
||||
</AuthProvider>
|
||||
<WsClientProvider conversationId="test-conversation-id">
|
||||
{children}
|
||||
</WsClientProvider>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
@ -3,18 +3,15 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
|
||||
describe("useSaveSettings", () => {
|
||||
it("should send an empty string for llm_api_key if an empty string is passed, otherwise undefined", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const { result } = renderHook(() => useSaveSettings(), {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
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 App from "#/routes/conversation";
|
||||
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" },
|
||||
]);
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("#/hooks/use-terminal", () => ({
|
||||
useTerminal: vi.fn(),
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", async () => {
|
||||
renderWithProviders(<RouteStub initialEntries={["/conversation/123"]} />);
|
||||
await screen.findByTestId("app-route");
|
||||
});
|
||||
});
|
||||
@ -5,7 +5,6 @@ import userEvent from "@testing-library/user-event";
|
||||
import AppSettingsScreen from "#/routes/app-settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
@ -14,7 +13,7 @@ const renderAppSettingsScreen = () =>
|
||||
render(<AppSettingsScreen />, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
@ -6,7 +6,6 @@ import userEvent from "@testing-library/user-event";
|
||||
import GitSettingsScreen from "#/routes/git-settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
import { SecretsService } from "#/api/secrets-service";
|
||||
@ -46,7 +45,7 @@ const renderGitSettingsScreen = () => {
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
},
|
||||
@ -55,9 +54,7 @@ const renderGitSettingsScreen = () => {
|
||||
const rerenderGitSettingsScreen = () =>
|
||||
rerender(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<GitSettingsRouterStub initialEntries={["/settings/github"]} />
|
||||
</AuthProvider>
|
||||
<GitSettingsRouterStub initialEntries={["/settings/github"]} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
@ -141,8 +138,8 @@ describe("Content", () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: null,
|
||||
gitlab: null,
|
||||
github: "some-token",
|
||||
gitlab: "some-token",
|
||||
},
|
||||
});
|
||||
queryClient.invalidateQueries();
|
||||
@ -166,7 +163,7 @@ describe("Content", () => {
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
gitlab: null,
|
||||
gitlab: "some-token",
|
||||
},
|
||||
});
|
||||
queryClient.invalidateQueries();
|
||||
@ -293,6 +290,7 @@ describe("Form submission", () => {
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: null,
|
||||
gitlab: "some-token",
|
||||
},
|
||||
});
|
||||
|
||||
@ -323,6 +321,7 @@ describe("Form submission", () => {
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: null,
|
||||
gitlab: "some-token",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -6,10 +6,10 @@ import { createRoutesStub } from "react-router";
|
||||
import { Provider } from "react-redux";
|
||||
import { createAxiosNotFoundErrorObject, setupStore } from "test-utils";
|
||||
import HomeScreen from "#/routes/home";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import MainApp from "#/routes/root-layout";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
@ -32,15 +32,13 @@ const RouterStub = createRoutesStub([
|
||||
},
|
||||
]);
|
||||
|
||||
const renderHomeScreen = (initialProvidersAreSet = true) =>
|
||||
const renderHomeScreen = () =>
|
||||
render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<AuthProvider initialProvidersAreSet={initialProvidersAreSet}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
),
|
||||
});
|
||||
@ -61,6 +59,17 @@ const MOCK_RESPOSITORIES: GitRepository[] = [
|
||||
];
|
||||
|
||||
describe("HomeScreen", () => {
|
||||
beforeEach(() => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: "some-token",
|
||||
gitlab: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderHomeScreen();
|
||||
screen.getByTestId("home-screen");
|
||||
@ -69,8 +78,10 @@ describe("HomeScreen", () => {
|
||||
it("should render the repository connector and suggested tasks sections", async () => {
|
||||
renderHomeScreen();
|
||||
|
||||
screen.getByTestId("repo-connector");
|
||||
screen.getByTestId("task-suggestions");
|
||||
await waitFor(() => {
|
||||
screen.getByTestId("repo-connector");
|
||||
screen.getByTestId("task-suggestions");
|
||||
});
|
||||
});
|
||||
|
||||
it("should have responsive layout for mobile and desktop screens", async () => {
|
||||
@ -91,7 +102,7 @@ describe("HomeScreen", () => {
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
const taskSuggestions = screen.getByTestId("task-suggestions");
|
||||
const taskSuggestions = await screen.findByTestId("task-suggestions");
|
||||
|
||||
// Initially, all tasks should be visible
|
||||
await waitFor(() => {
|
||||
@ -126,7 +137,7 @@ describe("HomeScreen", () => {
|
||||
|
||||
renderHomeScreen();
|
||||
|
||||
const taskSuggestions = screen.getByTestId("task-suggestions");
|
||||
const taskSuggestions = await screen.findByTestId("task-suggestions");
|
||||
|
||||
// Initially, all tasks should be visible
|
||||
await waitFor(() => {
|
||||
@ -164,7 +175,7 @@ describe("HomeScreen", () => {
|
||||
describe("launch buttons", () => {
|
||||
const setupLaunchButtons = async () => {
|
||||
let headerLaunchButton = screen.getByTestId("header-launch-button");
|
||||
let repoLaunchButton = screen.getByTestId("repo-launch-button");
|
||||
let repoLaunchButton = await screen.findByTestId("repo-launch-button");
|
||||
let tasksLaunchButtons =
|
||||
await screen.findAllByTestId("task-launch-button");
|
||||
|
||||
@ -256,7 +267,7 @@ describe("HomeScreen", () => {
|
||||
});
|
||||
|
||||
it("should hide the suggested tasks section if not authed with git(hub|lab)", async () => {
|
||||
renderHomeScreen(false);
|
||||
renderHomeScreen();
|
||||
|
||||
const taskSuggestions = screen.queryByTestId("task-suggestions");
|
||||
const repoConnector = screen.getByTestId("repo-connector");
|
||||
@ -267,6 +278,10 @@ describe("HomeScreen", () => {
|
||||
});
|
||||
|
||||
describe("Settings 404", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
MOCK_DEFAULT_USER_SETTINGS,
|
||||
resetTestHandlersMockSettings,
|
||||
} from "#/mocks/handlers";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
|
||||
@ -16,7 +15,7 @@ const renderLlmSettingsScreen = () =>
|
||||
render(<LlmSettingsScreen />, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
@ -2,24 +2,24 @@ import { render, screen, within } from "@testing-library/react";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
// Mock the i18next hook
|
||||
vi.mock("react-i18next", async () => {
|
||||
const actual = await vi.importActual<typeof import("react-i18next")>("react-i18next");
|
||||
const actual =
|
||||
await vi.importActual<typeof import("react-i18next")>("react-i18next");
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"SETTINGS$NAV_GIT": "Git",
|
||||
"SETTINGS$NAV_APPLICATION": "Application",
|
||||
"SETTINGS$NAV_CREDITS": "Credits",
|
||||
"SETTINGS$NAV_API_KEYS": "API Keys",
|
||||
"SETTINGS$NAV_LLM": "LLM",
|
||||
"SETTINGS$TITLE": "Settings"
|
||||
SETTINGS$NAV_GIT: "Git",
|
||||
SETTINGS$NAV_APPLICATION: "Application",
|
||||
SETTINGS$NAV_CREDITS: "Credits",
|
||||
SETTINGS$NAV_API_KEYS: "API Keys",
|
||||
SETTINGS$NAV_LLM: "LLM",
|
||||
SETTINGS$TITLE: "Settings",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
@ -71,11 +71,9 @@ describe("Settings Screen", () => {
|
||||
const queryClient = new QueryClient();
|
||||
return render(<RouterStub initialEntries={[path]} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { convertRawProvidersToList } from "#/utils/convert-raw-providers-to-list";
|
||||
|
||||
describe("convertRawProvidersToList", () => {
|
||||
it("should convert raw provider tokens to a list of providers", () => {
|
||||
const example1: Partial<Record<Provider, string | null>> | undefined = {
|
||||
github: "test-token",
|
||||
gitlab: "test-token",
|
||||
};
|
||||
const example2: Partial<Record<Provider, string | null>> | undefined = {
|
||||
github: "",
|
||||
};
|
||||
const example3: Partial<Record<Provider, string | null>> | undefined = {
|
||||
gitlab: null,
|
||||
};
|
||||
|
||||
expect(convertRawProvidersToList(example1)).toEqual(["github", "gitlab"]);
|
||||
expect(convertRawProvidersToList(example2)).toEqual(["github"]);
|
||||
expect(convertRawProvidersToList(example3)).toEqual(["gitlab"]);
|
||||
});
|
||||
});
|
||||
@ -4,8 +4,8 @@ import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SuggestionItem } from "#/components/features/suggestions/suggestion-item";
|
||||
import type { RootState } from "#/store";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
|
||||
interface ActionSuggestionsProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@ -15,13 +15,14 @@ export function ActionSuggestions({
|
||||
onSuggestionsClick,
|
||||
}: ActionSuggestionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { providersAreSet } = useAuth();
|
||||
const { providers } = useUserProviders();
|
||||
const { selectedRepository } = useSelector(
|
||||
(state: RootState) => state.initialQuery,
|
||||
);
|
||||
|
||||
const [hasPullRequest, setHasPullRequest] = React.useState(false);
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const isGitLab =
|
||||
selectedRepository !== null &&
|
||||
selectedRepository.git_provider &&
|
||||
|
||||
@ -1,20 +1,21 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConnectToProviderMessage } from "./connect-to-provider-message";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { RepositorySelectionForm } from "./repo-selection-form";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { RepoProviderLinks } from "./repo-provider-links";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
|
||||
interface RepoConnectorProps {
|
||||
onRepoSelection: (repoTitle: string | null) => void;
|
||||
}
|
||||
|
||||
export function RepoConnector({ onRepoSelection }: RepoConnectorProps) {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { providers } = useUserProviders();
|
||||
const { data: config } = useConfig();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isSaaS = config?.APP_MODE === "saas";
|
||||
const providersAreSet = providers.length > 0;
|
||||
|
||||
return (
|
||||
<section
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useLocation } from "react-router";
|
||||
import { useGitUser } from "#/hooks/query/use-git-user";
|
||||
import { UserActions } from "./user-actions";
|
||||
@ -26,7 +25,7 @@ export function Sidebar() {
|
||||
isError: settingsIsError,
|
||||
isFetching: isFetchingSettings,
|
||||
} = useSettings();
|
||||
const { mutateAsync: logout } = useLogout();
|
||||
const { mutate: logout } = useLogout();
|
||||
|
||||
const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
|
||||
|
||||
@ -62,11 +61,6 @@ export function Sidebar() {
|
||||
location.pathname,
|
||||
]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
posthog.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1">
|
||||
@ -89,7 +83,7 @@ export function Sidebar() {
|
||||
user={
|
||||
user.data ? { avatar_url: user.data.avatar_url } : undefined
|
||||
}
|
||||
onLogout={handleLogout}
|
||||
onLogout={logout}
|
||||
isLoading={user.isFetching}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
import React from "react";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
interface AuthContextType {
|
||||
providerTokensSet: Provider[];
|
||||
setProviderTokensSet: (tokens: Provider[]) => void;
|
||||
providersAreSet: boolean;
|
||||
setProvidersAreSet: (status: boolean) => void;
|
||||
}
|
||||
|
||||
interface AuthContextProps extends React.PropsWithChildren {
|
||||
initialProviderTokens?: Provider[];
|
||||
initialProvidersAreSet?: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = React.createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
function AuthProvider({
|
||||
children,
|
||||
initialProviderTokens = [],
|
||||
initialProvidersAreSet = false,
|
||||
}: AuthContextProps) {
|
||||
const [providerTokensSet, setProviderTokensSet] = React.useState<Provider[]>(
|
||||
initialProviderTokens,
|
||||
);
|
||||
|
||||
const [providersAreSet, setProvidersAreSet] = React.useState<boolean>(
|
||||
initialProvidersAreSet,
|
||||
);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
providerTokensSet,
|
||||
setProviderTokensSet,
|
||||
providersAreSet,
|
||||
setProvidersAreSet,
|
||||
}),
|
||||
[providerTokensSet],
|
||||
);
|
||||
|
||||
return <AuthContext value={value}>{children}</AuthContext>;
|
||||
}
|
||||
|
||||
function useAuth() {
|
||||
const context = React.useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuth must be used within a AuthProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { AuthProvider, useAuth };
|
||||
@ -14,7 +14,7 @@ import {
|
||||
UserMessageAction,
|
||||
} from "#/types/core/actions";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useAuth } from "./auth-context";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
|
||||
const isOpenHandsEvent = (event: unknown): event is OpenHandsParsedEvent =>
|
||||
typeof event === "object" &&
|
||||
@ -128,7 +128,7 @@ export function WsClientProvider({
|
||||
);
|
||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||
const lastEventRef = React.useRef<Record<string, unknown> | null>(null);
|
||||
const { providerTokensSet } = useAuth();
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const messageRateHandler = useRate({ threshold: 250 });
|
||||
|
||||
@ -224,7 +224,7 @@ export function WsClientProvider({
|
||||
const query = {
|
||||
latest_event_id: lastEvent?.id ?? -1,
|
||||
conversation_id: conversationId,
|
||||
providers_set: providerTokensSet,
|
||||
providers_set: providers,
|
||||
};
|
||||
|
||||
const baseUrl =
|
||||
|
||||
@ -11,12 +11,11 @@ import { hydrateRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import "./i18n";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import store from "./store";
|
||||
import { AuthProvider } from "./context/auth-context";
|
||||
import { queryClientConfig } from "./query-client-config";
|
||||
import OpenHands from "./api/open-hands";
|
||||
import { displayErrorToast } from "./utils/custom-toast-handlers";
|
||||
import { queryClient } from "./query-client-config";
|
||||
|
||||
function PosthogInit() {
|
||||
const [posthogClientKey, setPosthogClientKey] = React.useState<string | null>(
|
||||
@ -59,20 +58,16 @@ async function prepareApp() {
|
||||
}
|
||||
}
|
||||
|
||||
export const queryClient = new QueryClient(queryClientConfig);
|
||||
|
||||
prepareApp().then(() =>
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<Provider store={store}>
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HydratedRouter />
|
||||
<PosthogInit />
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HydratedRouter />
|
||||
<PosthogInit />
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@ -1,38 +1,23 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate } from "react-router";
|
||||
import posthog from "posthog-js";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useConfig } from "../query/use-config";
|
||||
|
||||
export const useLogout = () => {
|
||||
const { setProviderTokensSet, setProvidersAreSet } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: config } = useConfig();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
// Pause all queries that depend on githubTokenIsSet
|
||||
queryClient.setQueryData(["user"], null);
|
||||
|
||||
// Call logout endpoint
|
||||
await OpenHands.logout(config?.APP_MODE ?? "oss");
|
||||
|
||||
// Remove settings from cache so it will be refetched with new token state
|
||||
queryClient.removeQueries({ queryKey: ["settings"] });
|
||||
|
||||
// Update token state - this will trigger a settings refetch since it's part of the query key
|
||||
setProviderTokensSet([]);
|
||||
setProvidersAreSet(false);
|
||||
|
||||
// Navigate to root page and refresh the page
|
||||
navigate("/");
|
||||
window.location.reload();
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Home screen suggested tasks
|
||||
queryClient.invalidateQueries({ queryKey: ["tasks"] });
|
||||
mutationFn: () => OpenHands.logout(config?.APP_MODE ?? "oss"),
|
||||
onSuccess: async () => {
|
||||
queryClient.removeQueries({ queryKey: ["tasks"] });
|
||||
queryClient.removeQueries({ queryKey: ["settings"] });
|
||||
queryClient.removeQueries({ queryKey: ["user"] });
|
||||
|
||||
posthog.reset();
|
||||
await navigate("/");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ApiKeysClient from "#/api/api-keys";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
export const API_KEYS_QUERY_KEY = "api-keys";
|
||||
|
||||
export function useApiKeys() {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [API_KEYS_QUERY_KEY],
|
||||
enabled: providersAreSet && config?.APP_MODE === "saas",
|
||||
enabled: config?.APP_MODE === "saas",
|
||||
queryFn: async () => {
|
||||
const keys = await ApiKeysClient.getApiKeys();
|
||||
return Array.isArray(keys) ? keys : [];
|
||||
|
||||
@ -3,19 +3,16 @@ import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useConfig } from "./use-config";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useLogout } from "../mutation/use-logout";
|
||||
import { useUserProviders } from "../use-user-providers";
|
||||
|
||||
export const useGitUser = () => {
|
||||
const { providersAreSet, providerTokensSet } = useAuth();
|
||||
const { mutateAsync: logout } = useLogout();
|
||||
|
||||
const { providers } = useUserProviders();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const user = useQuery({
|
||||
queryKey: ["user", providerTokensSet],
|
||||
queryKey: ["user"],
|
||||
queryFn: OpenHands.getGitUser,
|
||||
enabled: providersAreSet && !!config?.APP_MODE,
|
||||
enabled: !!config?.APP_MODE && providers.length > 0,
|
||||
retry: false,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
@ -33,16 +30,5 @@ export const useGitUser = () => {
|
||||
}
|
||||
}, [user.data]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
posthog.reset();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user.isError) {
|
||||
handleLogout();
|
||||
}
|
||||
}, [user.isError]);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
@ -1,19 +1,16 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||
|
||||
export const useIsAuthed = () => {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
const isOnTosPage = useIsOnTosPage();
|
||||
|
||||
const appMode = React.useMemo(() => config?.APP_MODE, [config]);
|
||||
const appMode = config?.APP_MODE;
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["user", "authenticated", providersAreSet, appMode],
|
||||
queryKey: ["user", "authenticated", appMode],
|
||||
queryFn: () => OpenHands.authenticate(appMode!),
|
||||
enabled: !!appMode && !isOnTosPage,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
|
||||
@ -2,10 +2,10 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||
import { Settings } from "#/types/settings";
|
||||
import { useIsAuthed } from "./use-is-authed";
|
||||
|
||||
const getSettingsQueryFn = async (): Promise<Settings> => {
|
||||
const apiSettings = await OpenHands.getSettings();
|
||||
@ -30,13 +30,11 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
|
||||
};
|
||||
|
||||
export const useSettings = () => {
|
||||
const { setProviderTokensSet, providerTokensSet, setProvidersAreSet } =
|
||||
useAuth();
|
||||
|
||||
const isOnTosPage = useIsOnTosPage();
|
||||
const { data: userIsAuthenticated } = useIsAuthed();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["settings", providerTokensSet],
|
||||
queryKey: ["settings"],
|
||||
queryFn: getSettingsQueryFn,
|
||||
// Only retry if the error is not a 404 because we
|
||||
// would want to show the modal immediately if the
|
||||
@ -44,7 +42,7 @@ export const useSettings = () => {
|
||||
retry: (_, error) => error.status !== 404,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
enabled: !isOnTosPage,
|
||||
enabled: !isOnTosPage && !!userIsAuthenticated,
|
||||
meta: {
|
||||
disableToast: true,
|
||||
},
|
||||
@ -56,18 +54,6 @@ export const useSettings = () => {
|
||||
}
|
||||
}, [query.data?.LLM_API_KEY_SET, query.isFetched]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (query.data?.PROVIDER_TOKENS_SET) {
|
||||
const providers = query.data.PROVIDER_TOKENS_SET;
|
||||
const setProviders = Object.keys(providers) as Array<
|
||||
keyof typeof providers
|
||||
>;
|
||||
setProviderTokensSet(setProviders);
|
||||
const atLeastOneSet = setProviders.length > 0;
|
||||
setProvidersAreSet(atLeastOneSet);
|
||||
}
|
||||
}, [query.data?.PROVIDER_TOKENS_SET, query.isFetched]);
|
||||
|
||||
// We want to return the defaults if the settings aren't found so the user can still see the
|
||||
// options to make their initial save. We don't set the defaults in `initialData` above because
|
||||
// that would prepopulate the data to the cache and mess with expectations. Read more:
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { SuggestionsService } from "#/api/suggestions-service/suggestions-service.api";
|
||||
import { groupSuggestedTasks } from "#/utils/group-suggested-tasks";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useIsAuthed } from "./use-is-authed";
|
||||
|
||||
export const useSuggestedTasks = () => {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { data: userIsAuthenticated } = useIsAuthed();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["tasks"],
|
||||
queryFn: SuggestionsService.getSuggestedTasks,
|
||||
select: groupSuggestedTasks,
|
||||
enabled: providersAreSet,
|
||||
enabled: !!userIsAuthenticated,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,15 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
export const useUserRepositories = () => {
|
||||
const { providerTokensSet, providersAreSet } = useAuth();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["repositories", providerTokensSet],
|
||||
export const useUserRepositories = () =>
|
||||
useQuery({
|
||||
queryKey: ["repositories"],
|
||||
queryFn: OpenHands.retrieveUserGitRepositories,
|
||||
enabled: providersAreSet,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import { useLogout } from "./mutation/use-logout";
|
||||
|
||||
export const useAppLogout = () => {
|
||||
const { mutateAsync: logout } = useLogout();
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
};
|
||||
|
||||
return { handleLogout };
|
||||
};
|
||||
@ -1,7 +1,5 @@
|
||||
import React from "react";
|
||||
import { generateAuthUrl } from "#/utils/generate-auth-url";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
interface UseAuthUrlConfig {
|
||||
appMode: GetConfigResponse["APP_MODE"] | null;
|
||||
@ -9,21 +7,12 @@ interface UseAuthUrlConfig {
|
||||
}
|
||||
|
||||
export const useAuthUrl = (config: UseAuthUrlConfig) => {
|
||||
const { providersAreSet } = useAuth();
|
||||
if (config.appMode === "saas") {
|
||||
return generateAuthUrl(
|
||||
config.identityProvider,
|
||||
new URL(window.location.href),
|
||||
);
|
||||
}
|
||||
|
||||
return React.useMemo(() => {
|
||||
if (config.appMode === "saas" && !providersAreSet) {
|
||||
try {
|
||||
return generateAuthUrl(
|
||||
config.identityProvider,
|
||||
new URL(window.location.href),
|
||||
);
|
||||
} catch (e) {
|
||||
// In test environment, window.location.href might not be a valid URL
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [providersAreSet, config.appMode, config.identityProvider]);
|
||||
return null;
|
||||
};
|
||||
|
||||
9
frontend/src/hooks/use-user-providers.ts
Normal file
9
frontend/src/hooks/use-user-providers.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { convertRawProvidersToList } from "#/utils/convert-raw-providers-to-list";
|
||||
import { useSettings } from "./query/use-settings";
|
||||
|
||||
export const useUserProviders = () => {
|
||||
const { data: settings } = useSettings();
|
||||
return {
|
||||
providers: convertRawProvidersToList(settings?.PROVIDER_TOKENS_SET),
|
||||
};
|
||||
};
|
||||
@ -1,17 +1,26 @@
|
||||
import {
|
||||
QueryClientConfig,
|
||||
QueryCache,
|
||||
MutationCache,
|
||||
} from "@tanstack/react-query";
|
||||
import { QueryCache, MutationCache, QueryClient } from "@tanstack/react-query";
|
||||
import i18next from "i18next";
|
||||
import { AxiosError } from "axios";
|
||||
import { I18nKey } from "./i18n/declaration";
|
||||
import { retrieveAxiosErrorMessage } from "./utils/retrieve-axios-error-message";
|
||||
import { displayErrorToast } from "./utils/custom-toast-handlers";
|
||||
|
||||
const handle401Error = (error: AxiosError, queryClient: QueryClient) => {
|
||||
if (error?.response?.status === 401 || error?.status === 401) {
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "authenticated"] });
|
||||
}
|
||||
};
|
||||
|
||||
const shownErrors = new Set<string>();
|
||||
export const queryClientConfig: QueryClientConfig = {
|
||||
export const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: (error, query) => {
|
||||
const isAuthQuery =
|
||||
query.queryKey[0] === "user" && query.queryKey[1] === "authenticated";
|
||||
if (!isAuthQuery) {
|
||||
handle401Error(error, queryClient);
|
||||
}
|
||||
|
||||
if (!query.meta?.disableToast) {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
|
||||
@ -28,10 +37,12 @@ export const queryClientConfig: QueryClientConfig = {
|
||||
}),
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error, _, __, mutation) => {
|
||||
handle401Error(error, queryClient);
|
||||
|
||||
if (!mutation?.meta?.disableToast) {
|
||||
const message = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(message || i18next.t(I18nKey.ERROR$GENERIC));
|
||||
}
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@ -14,8 +14,8 @@ import {
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
import { GitSettingInputsSkeleton } from "#/components/features/settings/git-settings/github-settings-inputs-skeleton";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useAddGitProviders } from "#/hooks/mutation/use-add-git-providers";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
|
||||
function GitSettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
@ -23,7 +23,7 @@ function GitSettingsScreen() {
|
||||
const { mutate: saveGitProviders, isPending } = useAddGitProviders();
|
||||
const { mutate: disconnectGitTokens } = useLogout();
|
||||
|
||||
const { providerTokensSet } = useAuth();
|
||||
const { providers } = useUserProviders();
|
||||
const { isLoading } = useSettings();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
@ -33,8 +33,8 @@ function GitSettingsScreen() {
|
||||
React.useState(false);
|
||||
|
||||
const isSaas = config?.APP_MODE === "saas";
|
||||
const isGitHubTokenSet = providerTokensSet.includes("github");
|
||||
const isGitLabTokenSet = providerTokensSet.includes("gitlab");
|
||||
const isGitHubTokenSet = providers.includes("github");
|
||||
const isGitLabTokenSet = providers.includes("gitlab");
|
||||
|
||||
const formAction = async (formData: FormData) => {
|
||||
const disconnectButtonClicked =
|
||||
|
||||
@ -3,16 +3,18 @@ import { PrefetchPageLinks } from "react-router";
|
||||
import { HomeHeader } from "#/components/features/home/home-header";
|
||||
import { RepoConnector } from "#/components/features/home/repo-connector";
|
||||
import { TaskSuggestions } from "#/components/features/home/tasks/task-suggestions";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
|
||||
<PrefetchPageLinks page="/conversations/:conversationId" />;
|
||||
|
||||
function HomeScreen() {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { providers } = useUserProviders();
|
||||
const [selectedRepoTitle, setSelectedRepoTitle] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="home-screen"
|
||||
|
||||
14
frontend/src/utils/convert-raw-providers-to-list.ts
Normal file
14
frontend/src/utils/convert-raw-providers-to-list.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
export const convertRawProvidersToList = (
|
||||
raw: Partial<Record<Provider, string | null>> | undefined,
|
||||
): Provider[] => {
|
||||
if (!raw) return [];
|
||||
const list: Provider[] = [];
|
||||
for (const key of Object.keys(raw)) {
|
||||
if (key) {
|
||||
list.push(key as Provider);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
};
|
||||
@ -10,7 +10,6 @@ import i18n from "i18next";
|
||||
import { vi } from "vitest";
|
||||
import { AxiosError } from "axios";
|
||||
import { AppStore, RootState, rootReducer } from "./src/store";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { ConversationProvider } from "#/context/conversation-context";
|
||||
|
||||
// Mock useParams before importing components
|
||||
@ -66,19 +65,17 @@ export function renderWithProviders(
|
||||
function Wrapper({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<AuthProvider initialProviderTokens={[]}>
|
||||
<QueryClientProvider
|
||||
client={
|
||||
new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
})
|
||||
}
|
||||
>
|
||||
<ConversationProvider>
|
||||
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
|
||||
</ConversationProvider>
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
<QueryClientProvider
|
||||
client={
|
||||
new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
})
|
||||
}
|
||||
>
|
||||
<ConversationProvider>
|
||||
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
|
||||
</ConversationProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user