mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
fix: restore local integration token removal in OSS settings (#14155)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
This commit is contained in:
@@ -389,6 +389,7 @@ There are two main patterns for saving settings in the OpenHands frontend:
|
||||
**When to use each pattern:**
|
||||
- Use Pattern 1 (Immediate Save) for entity management where each item is independent
|
||||
- Use Pattern 2 (Manual Save) for configuration forms where settings are interdependent or need validation
|
||||
- Git provider tokens in the local/OSS integrations settings are managed through the V1 secrets endpoints (`POST`/`DELETE /api/v1/secrets/git-providers`). Do not reuse the logout flow for disconnecting tokens; `useLogout` is for actual app logout and still targets legacy OSS logout behavior.
|
||||
|
||||
### Adding New LLM Models
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SecretsService } from "#/api/secrets-service";
|
||||
import { useDeleteGitProviders } from "#/hooks/mutation/use-delete-git-providers";
|
||||
|
||||
vi.mock("#/context/use-selected-organization", () => ({
|
||||
useSelectedOrganizationId: () => ({ organizationId: "org-1" }),
|
||||
}));
|
||||
|
||||
describe("useDeleteGitProviders", () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("invalidates personal settings queries after deleting providers", async () => {
|
||||
vi.spyOn(SecretsService, "deleteGitProviders").mockResolvedValue(true);
|
||||
|
||||
const personalSettingsQueryKey = ["settings", "personal", "org-1"] as const;
|
||||
queryClient.setQueryData(personalSettingsQueryKey, {
|
||||
provider_tokens_set: { github: null },
|
||||
});
|
||||
|
||||
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useDeleteGitProviders(), { wrapper });
|
||||
|
||||
await result.current.mutateAsync();
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalled();
|
||||
expect(queryClient.getQueryState(personalSettingsQueryKey)?.isInvalidated).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import { I18nextProvider } from "react-i18next";
|
||||
import GitSettingsScreen, { clientLoader } from "#/routes/git-settings";
|
||||
import SettingsService from "#/api/settings-service/settings-service.api";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import AuthService from "#/api/auth-service/auth-service.api";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import { WebClientConfig } from "#/api/option-service/option.types";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
@@ -484,11 +483,12 @@ describe("Form submission", () => {
|
||||
await waitFor(() => expect(disconnectButton).toBeDisabled());
|
||||
});
|
||||
|
||||
it("should call logout when pressing the disconnect tokens button", async () => {
|
||||
it("should delete git providers when pressing the disconnect tokens button", async () => {
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
const logoutSpy = vi.spyOn(AuthService, "logout");
|
||||
const deleteGitProvidersSpy = vi.spyOn(SecretsService, "deleteGitProviders");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
|
||||
deleteGitProvidersSpy.mockResolvedValue(true);
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
@@ -506,7 +506,7 @@ describe("Form submission", () => {
|
||||
await waitFor(() => expect(disconnectButton).not.toBeDisabled());
|
||||
await userEvent.click(disconnectButton);
|
||||
|
||||
expect(logoutSpy).toHaveBeenCalled();
|
||||
expect(deleteGitProvidersSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// flaky test
|
||||
|
||||
@@ -94,4 +94,9 @@ export class SecretsService {
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
static async deleteGitProviders() {
|
||||
const { status } = await openHands.delete("/api/v1/secrets/git-providers");
|
||||
return status === 200;
|
||||
}
|
||||
}
|
||||
|
||||
20
frontend/src/hooks/mutation/use-delete-git-providers.ts
Normal file
20
frontend/src/hooks/mutation/use-delete-git-providers.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSelectedOrganizationId } from "#/context/use-selected-organization";
|
||||
import { SecretsService } from "#/api/secrets-service";
|
||||
|
||||
export const useDeleteGitProviders = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { organizationId } = useSelectedOrganizationId();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => SecretsService.deleteGitProviders(),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["settings", "personal", organizationId],
|
||||
});
|
||||
},
|
||||
meta: {
|
||||
disableToast: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { useConfig } from "#/hooks/query/use-config";
|
||||
import { createPermissionGuard } from "#/utils/org/permission-guard";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { useLogout } from "#/hooks/mutation/use-logout";
|
||||
import { useDeleteGitProviders } from "#/hooks/mutation/use-delete-git-providers";
|
||||
import { GitHubTokenInput } from "#/components/features/settings/git-settings/github-token-input";
|
||||
import { GitLabTokenInput } from "#/components/features/settings/git-settings/gitlab-token-input";
|
||||
import { GitLabWebhookManager } from "#/components/features/settings/git-settings/gitlab-webhook-manager";
|
||||
@@ -33,7 +33,8 @@ function GitSettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { mutate: saveGitProviders, isPending } = useAddGitProviders();
|
||||
const { mutate: disconnectGitTokens } = useLogout();
|
||||
const { mutate: disconnectGitTokens, isPending: isDisconnecting } =
|
||||
useDeleteGitProviders();
|
||||
|
||||
const { data: settings, isLoading } = useSettings();
|
||||
const { providers } = useUserProviders();
|
||||
@@ -87,7 +88,15 @@ function GitSettingsScreen() {
|
||||
formData.get("disconnect-tokens-button") !== null;
|
||||
|
||||
if (disconnectButtonClicked) {
|
||||
disconnectGitTokens();
|
||||
disconnectGitTokens(undefined, {
|
||||
onSuccess: () => {
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$SAVED));
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC));
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -355,12 +364,13 @@ function GitSettingsScreen() {
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
isDisabled={
|
||||
!isGitHubTokenSet &&
|
||||
!isGitLabTokenSet &&
|
||||
!isBitbucketTokenSet &&
|
||||
!isBitbucketDCTokenSet &&
|
||||
!isAzureDevOpsTokenSet &&
|
||||
!isForgejoTokenSet
|
||||
isDisconnecting ||
|
||||
(!isGitHubTokenSet &&
|
||||
!isGitLabTokenSet &&
|
||||
!isBitbucketTokenSet &&
|
||||
!isBitbucketDCTokenSet &&
|
||||
!isAzureDevOpsTokenSet &&
|
||||
!isForgejoTokenSet)
|
||||
}
|
||||
>
|
||||
{t(I18nKey.GIT$DISCONNECT_TOKENS)}
|
||||
|
||||
Reference in New Issue
Block a user