From 727a105ed5eddcc4964983420ec4d5fce215132f Mon Sep 17 00:00:00 2001 From: Weves Date: Wed, 10 Sep 2025 12:00:18 -0700 Subject: [PATCH 1/7] Fix Connectors section in User Settings --- web/admin2_auth.json | 4 +- web/src/app/assistants/SidebarWrapper.tsx | 10 +- web/src/app/chat/components/ChatPage.tsx | 8 ++ .../e2e/connectors/federated_slack.spec.ts | 108 ++++++++++++++++++ 4 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 web/tests/e2e/connectors/federated_slack.spec.ts diff --git a/web/admin2_auth.json b/web/admin2_auth.json index 7b17ebfb687..177ffaf9d4b 100644 --- a/web/admin2_auth.json +++ b/web/admin2_auth.json @@ -2,10 +2,10 @@ "cookies": [ { "name": "fastapiusersauth", - "value": "h3qhacpHbE4_09HcOLlVW4lSee48m1UbjYTUiKYwNiw", + "value": "AAdfmA4NT0BXdlg80K3xVuLGVPPPqH7JfzVk82otIw0", "domain": "localhost", "path": "/", - "expires": 1745624493.119168, + "expires": 1758133751.1961, "httpOnly": true, "secure": false, "sameSite": "Lax" diff --git a/web/src/app/assistants/SidebarWrapper.tsx b/web/src/app/assistants/SidebarWrapper.tsx index ebf0cdfc8a6..3eb62c41a12 100644 --- a/web/src/app/assistants/SidebarWrapper.tsx +++ b/web/src/app/assistants/SidebarWrapper.tsx @@ -15,6 +15,7 @@ import { useSidebarShortcut } from "@/lib/browserUtilities"; import { UserSettingsModal } from "@/app/chat/components/modal/UserSettingsModal"; import { usePopup } from "@/components/admin/connectors/Popup"; import { useUser } from "@/components/user/UserProvider"; +import { useFederatedOAuthStatus } from "@/lib/hooks/useFederatedOAuthStatus"; interface SidebarWrapperProps { size?: "sm" | "lg"; @@ -41,7 +42,11 @@ export default function SidebarWrapper({ }, [sidebarVisible]); const sidebarElementRef = useRef(null); - const { folders, openedFolders, chatSessions } = useChatContext(); + const { folders, openedFolders, chatSessions, ccPairs } = useChatContext(); + const { + connectors: federatedConnectors, + refetch: refetchFederatedConnectors, + } = useFederatedOAuthStatus(); const explicitlyUntoggle = () => { setShowDocSidebar(false); @@ -114,6 +119,9 @@ export default function SidebarWrapper({ setUserSettingsToggled(false)} defaultModel={user?.preferences?.default_model!} /> diff --git a/web/src/app/chat/components/ChatPage.tsx b/web/src/app/chat/components/ChatPage.tsx index f23ad136f31..624e4506a47 100644 --- a/web/src/app/chat/components/ChatPage.tsx +++ b/web/src/app/chat/components/ChatPage.tsx @@ -18,6 +18,7 @@ import { import { usePopup } from "@/components/admin/connectors/Popup"; import { SEARCH_PARAM_NAMES } from "../services/searchParams"; import { useFederatedConnectors, useFilters, useLlmManager } from "@/lib/hooks"; +import { useFederatedOAuthStatus } from "@/lib/hooks/useFederatedOAuthStatus"; import { FeedbackType } from "@/app/chat/interfaces"; import { OnyxInitializingLoader } from "@/components/OnyxInitializingLoader"; import { FeedbackModal } from "./modal/FeedbackModal"; @@ -161,6 +162,10 @@ export function ChatPage({ // Also fetch federated connectors for the sources list const { data: federatedConnectorsData } = useFederatedConnectors(); + const { + connectors: federatedConnectorOAuthStatus, + refetch: refetchFederatedConnectors, + } = useFederatedOAuthStatus(); const { user, isAdmin } = useUser(); const existingChatIdRaw = searchParams?.get("chatId"); @@ -796,6 +801,9 @@ export function ChatPage({ updateCurrentLlm={llmManager.updateCurrentLlm} defaultModel={user?.preferences.default_model!} llmProviders={llmProviders} + ccPairs={ccPairs} + federatedConnectors={federatedConnectorOAuthStatus} + refetchFederatedConnectors={refetchFederatedConnectors} onClose={() => { setUserSettingsToggled(false); setSettingsToggled(false); diff --git a/web/tests/e2e/connectors/federated_slack.spec.ts b/web/tests/e2e/connectors/federated_slack.spec.ts new file mode 100644 index 00000000000..23e01a43f71 --- /dev/null +++ b/web/tests/e2e/connectors/federated_slack.spec.ts @@ -0,0 +1,108 @@ +import { test, expect } from "@chromatic-com/playwright"; +import type { Page } from "@playwright/test"; +import { loginAs, loginAsRandomUser } from "../utils/auth"; + +test.use({ storageState: "admin_auth.json" }); + +const SLACK_CLIENT_ID = process.env.SLACK_CLIENT_ID || ""; +const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET || ""; + +async function createFederatedSlackConnector(page: Page) { + // Navigate to add connector page + await page.goto("http://localhost:3000/admin/add-connector"); + await page.waitForLoadState("networkidle"); + + // Click on Slack connector tile (specifically the one with "Logo Slack" text, not "Slack Bots") + await page.getByRole("link", { name: "Logo Slack" }).first().click(); + await page.waitForLoadState("networkidle"); + + // Fill in the client ID and client secret + await page.getByLabel(/client id/i).fill(SLACK_CLIENT_ID); + await page.getByLabel(/client secret/i).fill(SLACK_CLIENT_SECRET); + + // Submit the form to create or update the federated connector + const createOrUpdateButton = await page.getByRole("button", { + name: /create|update/i, + }); + await createOrUpdateButton.click(); + + // Wait for success message or redirect + await page.waitForTimeout(2000); +} + +async function navigateToUserSettings(page: Page) { + // Wait for any existing modals to close + await page.waitForTimeout(1000); + + // Wait for potential modal backdrop to disappear + await page + .waitForSelector(".fixed.inset-0.bg-neutral-950\\/50", { + state: "detached", + timeout: 5000, + }) + .catch(() => {}); + + // Click on user dropdown/settings button + await page.locator("#onyx-user-dropdown").click(); + + // Click on settings option + await page.getByText("User Settings").click(); + + // Wait for settings modal to appear + await expect(page.locator("h2", { hasText: "User Settings" })).toBeVisible(); +} + +async function openConnectorsTab(page: Page) { + // Click on the Connectors tab in user settings + await page.getByRole("button", { name: "Connectors" }).click(); + + // Wait for connectors section to be visible + // Allow multiple instances of "Connected Services" to be visible + const connectedServicesLocators = page.getByText("Connected Services"); + await expect(connectedServicesLocators.first()).toBeVisible(); +} + +test("Federated Slack Connector - Create, OAuth Modal, and User Settings Flow", async ({ + page, +}) => { + // Setup: Clear cookies and log in as a random user + await page.context().clearCookies(); + await loginAs(page, "admin"); + + // Create a federated Slack connector in admin panel + await createFederatedSlackConnector(page); + + // Log in as a random user + await page.context().clearCookies(); + await loginAsRandomUser(page); + + // Navigate back to main page and verify OAuth modal appears + await page.goto("http://localhost:3000/chat"); + await page.waitForLoadState("networkidle"); + + // Check if the OAuth modal appears + await expect( + page.getByText(/improve answer quality by letting/i) + ).toBeVisible({ timeout: 10000 }); + await expect(page.getByText(/slack/i)).toBeVisible(); + + // Decline the OAuth connection + await page.getByRole("button", { name: "Skip for now" }).click(); + + // Wait for modal to disappear + await expect( + page.getByText(/improve answer quality by letting/i) + ).not.toBeVisible(); + + // Go to user settings and verify the connector appears + await navigateToUserSettings(page); + await openConnectorsTab(page); + + // Verify Slack connector appears in the federated connectors section + await expect(page.getByText("Federated Connectors")).toBeVisible(); + await expect(page.getByText("Slack")).toBeVisible(); + await expect(page.getByText("Not connected")).toBeVisible(); + + // Verify there's a Connect button available + await expect(page.locator("button", { hasText: /^Connect$/ })).toBeVisible(); +}); From 6a2dc4e7595a028a5f4f3ce8b3e04fd2ef474515 Mon Sep 17 00:00:00 2001 From: Weves Date: Wed, 10 Sep 2025 12:01:30 -0700 Subject: [PATCH 2/7] Add to acitons --- .github/workflows/pr-playwright-tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pr-playwright-tests.yml b/.github/workflows/pr-playwright-tests.yml index 900553251d4..c6611636c27 100644 --- a/.github/workflows/pr-playwright-tests.yml +++ b/.github/workflows/pr-playwright-tests.yml @@ -9,6 +9,11 @@ env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} GEN_AI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + + # for federated slack tests + SLACK_CLIENT_ID: ${{ secrets.SLACK_CLIENT_ID }} + SLACK_CLIENT_SECRET: ${{ secrets.SLACK_CLIENT_SECRET }} + MOCK_LLM_RESPONSE: true jobs: From 06f0e4e347929959920585f04f102a5ecacb5863 Mon Sep 17 00:00:00 2001 From: Weves Date: Wed, 10 Sep 2025 12:03:34 -0700 Subject: [PATCH 3/7] Cleanup --- web/src/app/assistants/SidebarWrapper.tsx | 3 ++- web/src/app/chat/components/modal/UserSettingsModal.tsx | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/web/src/app/assistants/SidebarWrapper.tsx b/web/src/app/assistants/SidebarWrapper.tsx index 3eb62c41a12..b6b92b08eae 100644 --- a/web/src/app/assistants/SidebarWrapper.tsx +++ b/web/src/app/assistants/SidebarWrapper.tsx @@ -42,11 +42,12 @@ export default function SidebarWrapper({ }, [sidebarVisible]); const sidebarElementRef = useRef(null); - const { folders, openedFolders, chatSessions, ccPairs } = useChatContext(); + const { folders, chatSessions, ccPairs } = useChatContext(); const { connectors: federatedConnectors, refetch: refetchFederatedConnectors, } = useFederatedOAuthStatus(); + const explicitlyUntoggle = () => { setShowDocSidebar(false); diff --git a/web/src/app/chat/components/modal/UserSettingsModal.tsx b/web/src/app/chat/components/modal/UserSettingsModal.tsx index 95934c1999c..d56cf41d958 100644 --- a/web/src/app/chat/components/modal/UserSettingsModal.tsx +++ b/web/src/app/chat/components/modal/UserSettingsModal.tsx @@ -49,9 +49,9 @@ export function UserSettingsModal({ updateCurrentLlm?: (newOverride: LlmDescriptor) => void; onClose: () => void; defaultModel: string | null; - ccPairs?: CCPairBasicInfo[]; - federatedConnectors?: FederatedConnectorOAuthStatus[]; - refetchFederatedConnectors?: () => void; + ccPairs: CCPairBasicInfo[]; + federatedConnectors: FederatedConnectorOAuthStatus[]; + refetchFederatedConnectors: () => void; }) { const { refreshUser, From ab00050f8eb831fff2f0da20f3fc4cc067f38838 Mon Sep 17 00:00:00 2001 From: Chris Weaver Date: Wed, 10 Sep 2025 12:04:24 -0700 Subject: [PATCH 4/7] Update web/tests/e2e/connectors/federated_slack.spec.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- web/tests/e2e/connectors/federated_slack.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/tests/e2e/connectors/federated_slack.spec.ts b/web/tests/e2e/connectors/federated_slack.spec.ts index 23e01a43f71..414fce0de9d 100644 --- a/web/tests/e2e/connectors/federated_slack.spec.ts +++ b/web/tests/e2e/connectors/federated_slack.spec.ts @@ -65,7 +65,7 @@ async function openConnectorsTab(page: Page) { test("Federated Slack Connector - Create, OAuth Modal, and User Settings Flow", async ({ page, }) => { - // Setup: Clear cookies and log in as a random user + // Setup: Clear cookies and log in as admin await page.context().clearCookies(); await loginAs(page, "admin"); From 2f9ca82eb9f88eb3c75cc542dbffdd49af0f6d0c Mon Sep 17 00:00:00 2001 From: Weves Date: Wed, 10 Sep 2025 13:47:32 -0700 Subject: [PATCH 5/7] Cleanup federated slack connector --- web/admin2_auth.json | 4 +- .../e2e/connectors/federated_slack.spec.ts | 164 ++++++++++++++---- 2 files changed, 128 insertions(+), 40 deletions(-) diff --git a/web/admin2_auth.json b/web/admin2_auth.json index 177ffaf9d4b..610e2e97c56 100644 --- a/web/admin2_auth.json +++ b/web/admin2_auth.json @@ -2,10 +2,10 @@ "cookies": [ { "name": "fastapiusersauth", - "value": "AAdfmA4NT0BXdlg80K3xVuLGVPPPqH7JfzVk82otIw0", + "value": "3_1lUBEkRwurVz_5uWN-jimnGhCD9drBvVggguRyCZI", "domain": "localhost", "path": "/", - "expires": 1758133751.1961, + "expires": 1758141723.8561, "httpOnly": true, "secure": false, "sameSite": "Lax" diff --git a/web/tests/e2e/connectors/federated_slack.spec.ts b/web/tests/e2e/connectors/federated_slack.spec.ts index 414fce0de9d..7672473d904 100644 --- a/web/tests/e2e/connectors/federated_slack.spec.ts +++ b/web/tests/e2e/connectors/federated_slack.spec.ts @@ -62,47 +62,135 @@ async function openConnectorsTab(page: Page) { await expect(connectedServicesLocators.first()).toBeVisible(); } -test("Federated Slack Connector - Create, OAuth Modal, and User Settings Flow", async ({ - page, -}) => { - // Setup: Clear cookies and log in as admin - await page.context().clearCookies(); - await loginAs(page, "admin"); - - // Create a federated Slack connector in admin panel - await createFederatedSlackConnector(page); - - // Log in as a random user - await page.context().clearCookies(); - await loginAsRandomUser(page); - - // Navigate back to main page and verify OAuth modal appears - await page.goto("http://localhost:3000/chat"); +/** + * Cleanup function to delete the federated Slack connector from the admin panel + * This ensures test isolation by removing any test data created during the test + */ +async function deleteFederatedSlackConnector(page: Page) { + // Navigate to admin indexing status page + await page.goto("http://localhost:3000/admin/indexing/status"); await page.waitForLoadState("networkidle"); - // Check if the OAuth modal appears - await expect( - page.getByText(/improve answer quality by letting/i) - ).toBeVisible({ timeout: 10000 }); - await expect(page.getByText(/slack/i)).toBeVisible(); - - // Decline the OAuth connection - await page.getByRole("button", { name: "Skip for now" }).click(); - - // Wait for modal to disappear - await expect( - page.getByText(/improve answer quality by letting/i) - ).not.toBeVisible(); + // Expand the Slack section first (summary row toggles open on click) + const slackSummaryRow = page.locator("tr").filter({ + has: page.locator("text=/^\s*Slack\s*$/i"), + }); + if ((await slackSummaryRow.count()) > 0) { + await slackSummaryRow.first().click(); + // Wait a moment for rows to render + await page.waitForTimeout(500); + } + + // Look for the Slack federated connector row inside the expanded section + // The federated connectors have a "Federated Access" badge + const slackRow = page.locator("tr", { hasText: /federated access/i }); + + // Check if the connector exists + const rowCount = await slackRow.count(); + if (rowCount === 0) { + // No federated Slack connector found, nothing to delete + console.log("No federated Slack connector found to delete"); + return; + } + + // Click on the row to navigate to the detail page + await slackRow.first().click(); + await page.waitForLoadState("networkidle"); - // Go to user settings and verify the connector appears - await navigateToUserSettings(page); - await openConnectorsTab(page); + // Look for and click the delete button + // Open the Manage menu and click Delete + const manageButton = page.getByRole("button", { name: /manage/i }); + await manageButton + .waitFor({ state: "visible", timeout: 5000 }) + .catch(() => {}); + if (!(await manageButton.isVisible().catch(() => false))) { + console.log("Manage button not visible; skipping delete"); + return; + } + await manageButton.click(); + // Wait for the dropdown menu to appear and settle (Radix animation) + await page + .getByRole("menu") + .waitFor({ state: "visible", timeout: 3000 }) + .catch(() => {}); + await page.waitForTimeout(150); + + page.once("dialog", (dialog) => dialog.accept()); + const deleteMenuItem = page.getByRole("menuitem", { name: /^Delete$/ }); + await expect(deleteMenuItem).toBeVisible({ timeout: 5000 }); + await deleteMenuItem.click({ force: true }); + // Wait for deletion to complete and redirect + await page.waitForURL("**/admin/indexing/status*", { timeout: 15000 }); + await page.waitForLoadState("networkidle"); - // Verify Slack connector appears in the federated connectors section - await expect(page.getByText("Federated Connectors")).toBeVisible(); - await expect(page.getByText("Slack")).toBeVisible(); - await expect(page.getByText("Not connected")).toBeVisible(); + // Re-open Slack section and verify row is gone + if ((await slackSummaryRow.count()) > 0) { + await slackSummaryRow.first().click(); + await page.waitForTimeout(300); + } + const postDeleteSlackRow = page + .locator("tr") + .filter({ has: page.locator("text=/slack/i") }) + .filter({ hasText: /federated access/i }); + const remaining = await postDeleteSlackRow.count(); + if (remaining === 0) { + console.log("Federated Slack connector deleted successfully"); + } else { + console.log("Federated Slack connector still present after delete attempt"); + } +} - // Verify there's a Connect button available - await expect(page.locator("button", { hasText: /^Connect$/ })).toBeVisible(); +test("Federated Slack Connector - Create, OAuth Modal, and User Settings Flow", async ({ + page, +}) => { + try { + // Setup: Clear cookies and log in as admin + await page.context().clearCookies(); + await loginAs(page, "admin"); + + // Create a federated Slack connector in admin panel + await createFederatedSlackConnector(page); + + // Log in as a random user + await page.context().clearCookies(); + await loginAsRandomUser(page); + + // Navigate back to main page and verify OAuth modal appears + await page.goto("http://localhost:3000/chat"); + await page.waitForLoadState("networkidle"); + + // Check if the OAuth modal appears + await expect( + page.getByText(/improve answer quality by letting/i) + ).toBeVisible({ timeout: 10000 }); + await expect(page.getByText(/slack/i)).toBeVisible(); + + // Decline the OAuth connection + await page.getByRole("button", { name: "Skip for now" }).click(); + + // Wait for modal to disappear + await expect( + page.getByText(/improve answer quality by letting/i) + ).not.toBeVisible(); + + // Go to user settings and verify the connector appears + await navigateToUserSettings(page); + await openConnectorsTab(page); + + // Verify Slack connector appears in the federated connectors section + await expect(page.getByText("Federated Connectors")).toBeVisible(); + await expect(page.getByText("Slack")).toBeVisible(); + await expect(page.getByText("Not connected")).toBeVisible(); + + // Verify there's a Connect button available + await expect( + page.locator("button", { hasText: /^Connect$/ }) + ).toBeVisible(); + } finally { + // Cleanup: Delete the federated Slack connector + // Log back in as admin to delete the connector + await page.context().clearCookies(); + await loginAs(page, "admin"); + await deleteFederatedSlackConnector(page); + } }); From cbd078a61e3d98fba25ab7ae14e2c1dce28b9ae4 Mon Sep 17 00:00:00 2001 From: Weves Date: Wed, 10 Sep 2025 14:15:42 -0700 Subject: [PATCH 6/7] Fix --- web/tests/e2e/connectors/federated_slack.spec.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/web/tests/e2e/connectors/federated_slack.spec.ts b/web/tests/e2e/connectors/federated_slack.spec.ts index 7672473d904..6928b0b47aa 100644 --- a/web/tests/e2e/connectors/federated_slack.spec.ts +++ b/web/tests/e2e/connectors/federated_slack.spec.ts @@ -122,22 +122,6 @@ async function deleteFederatedSlackConnector(page: Page) { // Wait for deletion to complete and redirect await page.waitForURL("**/admin/indexing/status*", { timeout: 15000 }); await page.waitForLoadState("networkidle"); - - // Re-open Slack section and verify row is gone - if ((await slackSummaryRow.count()) > 0) { - await slackSummaryRow.first().click(); - await page.waitForTimeout(300); - } - const postDeleteSlackRow = page - .locator("tr") - .filter({ has: page.locator("text=/slack/i") }) - .filter({ hasText: /federated access/i }); - const remaining = await postDeleteSlackRow.count(); - if (remaining === 0) { - console.log("Federated Slack connector deleted successfully"); - } else { - console.log("Federated Slack connector still present after delete attempt"); - } } test("Federated Slack Connector - Create, OAuth Modal, and User Settings Flow", async ({ From 53ba48105808e87c9a936ac5c37f2687ec51bf0f Mon Sep 17 00:00:00 2001 From: Weves Date: Wed, 10 Sep 2025 19:27:00 -0700 Subject: [PATCH 7/7] Improve test --- web/playwright.config.ts | 3 +++ web/tests/e2e/connectors/federated_slack.spec.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/web/playwright.config.ts b/web/playwright.config.ts index 69c2f2c983e..43f60b06bdc 100644 --- a/web/playwright.config.ts +++ b/web/playwright.config.ts @@ -1,4 +1,7 @@ import { defineConfig, devices } from "@playwright/test"; +import * as dotenv from "dotenv"; + +dotenv.config({ path: ".vscode/.env" }); export default defineConfig({ globalSetup: require.resolve("./tests/e2e/global-setup"), diff --git a/web/tests/e2e/connectors/federated_slack.spec.ts b/web/tests/e2e/connectors/federated_slack.spec.ts index 6928b0b47aa..e7d3cde43e6 100644 --- a/web/tests/e2e/connectors/federated_slack.spec.ts +++ b/web/tests/e2e/connectors/federated_slack.spec.ts @@ -4,8 +4,8 @@ import { loginAs, loginAsRandomUser } from "../utils/auth"; test.use({ storageState: "admin_auth.json" }); -const SLACK_CLIENT_ID = process.env.SLACK_CLIENT_ID || ""; -const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET || ""; +const SLACK_CLIENT_ID = process.env.SLACK_CLIENT_ID; +const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET; async function createFederatedSlackConnector(page: Page) { // Navigate to add connector page @@ -16,6 +16,10 @@ async function createFederatedSlackConnector(page: Page) { await page.getByRole("link", { name: "Logo Slack" }).first().click(); await page.waitForLoadState("networkidle"); + if (!SLACK_CLIENT_ID || !SLACK_CLIENT_SECRET) { + throw new Error("SLACK_CLIENT_ID and SLACK_CLIENT_SECRET must be set"); + } + // Fill in the client ID and client secret await page.getByLabel(/client id/i).fill(SLACK_CLIENT_ID); await page.getByLabel(/client secret/i).fill(SLACK_CLIENT_SECRET);