From 52f0591073e2cf6b21180e9016f4ce1a512e59ff Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Mon, 27 Oct 2025 15:43:04 +0530 Subject: [PATCH 01/26] Add debug error and success JSP pages --- .../src/main/webapp/debugError.jsp | 123 ++++++++++++++++++ .../src/main/webapp/debugSuccess.jsp | 120 +++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 identity-apps-core/apps/authentication-portal/src/main/webapp/debugError.jsp create mode 100644 identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp diff --git a/identity-apps-core/apps/authentication-portal/src/main/webapp/debugError.jsp b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugError.jsp new file mode 100644 index 00000000000..38b5de28938 --- /dev/null +++ b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugError.jsp @@ -0,0 +1,123 @@ +<%-- + ~ Copyright (c) 2025, WSO2 LLC. + ~ Licensed under the Apache License, Version 2.0. + ~ See the License for the specific language governing permissions and limitations under the License. +--%> + +<%@ page import="org.apache.commons.text.StringEscapeUtils" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> +<%@include file="includes/localize.jsp" %> + + + + + + + + <%-- header include --%> + <% + java.io.File headerFile = new java.io.File(getServletContext().getRealPath("extensions/header.jsp")); + if (headerFile.exists()) { + %> + + <% } else { %> + + <% } %> + + +"> + + + + + <%-- product-title include --%> + <% + java.io.File productTitleFile = new java.io.File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + +
+

+ <%= AuthenticationEndpointUtil.i18n(resourceBundle, "Something went wrong!") %> +

+ +
+ +
+
+
+ + + <%-- product-footer include --%> + <% + java.io.File productFooterFile = new java.io.File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + + +
+ +<%-- footer include --%> +<% + java.io.File footerFile = new java.io.File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { +%> + +<% } else { %> + +<% } %> + + + + diff --git a/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp new file mode 100644 index 00000000000..7d4f8a11173 --- /dev/null +++ b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp @@ -0,0 +1,120 @@ +<%-- + ~ Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + ~ + ~ WSO2 LLC. licenses this file to you under the Apache License, + ~ Version 2.0 (the "License"); you may not use this file except + ~ in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. +--%> + +<%@ page import="java.io.File" %> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + +<%@include file="includes/localize.jsp" %> + + +<%-- Branding Preferences --%> + + +<% + request.setAttribute("pageName", "authentication-success"); +%> + + + + + <%-- header --%> + <% + File headerFile = new File(getServletContext().getRealPath("extensions/header.jsp")); + if (headerFile.exists()) { + %> + + <% } else { %> + + <% } %> + +"> + + + <%-- product-title --%> + <% + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { + %> + + <% } else { %> + + <% } %> + + +
+

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "Authentication successful")%>

+

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "You will be redirected shortly")%>...

+
+
+ + <%-- product-footer --%> + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + +
+ +<%-- footer --%> +<% + File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { +%> + +<% } else { %> + +<% } %> + + + + + From 8683099897f029a3d21405dea97119a16b842fed Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Sun, 2 Nov 2025 12:20:17 +0530 Subject: [PATCH 02/26] Add "Test Connection" button to Connection Edit page --- .../pages/connection-edit.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/features/admin.connections.v1/pages/connection-edit.tsx b/features/admin.connections.v1/pages/connection-edit.tsx index 4f2ead28f41..cfe3e8ecbda 100644 --- a/features/admin.connections.v1/pages/connection-edit.tsx +++ b/features/admin.connections.v1/pages/connection-edit.tsx @@ -66,6 +66,10 @@ import { CustomAuthConnectionInterface, SupportedQuickStartTemplateTypes } from "../models/connection"; +import { Icon } from "semantic-ui-react"; +import { + PrimaryButton, +} from "@wso2is/react-components"; import { ConnectionTemplateManagementUtils } from "../utils/connection-template-utils"; import { ConnectionsManagementUtils, handleGetConnectionsMetaDataError } from "../utils/connection-utils"; @@ -696,6 +700,25 @@ const ConnectionEditPage: FunctionComponent = imageType: "square" } } title={ resolveConnectorName(connector) } + action={ ( + { + // Get tenant domain from location.pathname + const pathParts = location.pathname.split("/"); + const tenantIndex = pathParts.indexOf("t"); + const tenantDomain = tenantIndex !== -1 ? pathParts[tenantIndex + 1] : "carbon.super"; + const idpId = connector?.id; + if (!idpId) return; + history.push(`/t/${tenantDomain}/console/connections/${idpId}/test`); + }} + > + + Test Connection + + ) } contentTopMargin={ true } description={ resolveConnectorDescription(connector) } image={ resolveConnectorImage(connector) } From d74523c7490292ff10e0a9247ab565f8a98682a3 Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Sun, 2 Nov 2025 12:20:53 +0530 Subject: [PATCH 03/26] Add Connection Test page for Identity Provider --- .../pages/connection-test.tsx | 334 ++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 features/admin.connections.v1/pages/connection-test.tsx diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx new file mode 100644 index 00000000000..0aa198a4b23 --- /dev/null +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -0,0 +1,334 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Connection Test page for Identity Provider. + */ + +import { history } from "@wso2is/admin.core.v1/helpers/history"; +import { + PageLayout, + PrimaryButton, + TabPageLayout, + AppAvatar, + AnimatedAvatar +} from "@wso2is/react-components"; +import React, { useState, useRef, Fragment } from "react"; +import useUIConfig from "@wso2is/admin.core.v1/hooks/use-ui-configs"; +import { useTranslation } from "react-i18next"; +import { RouteComponentProps } from "react-router-dom"; +import { Header, Icon, List, Segment } from "semantic-ui-react"; +import { ConnectionsManagementUtils } from "@wso2is/admin.connections.v1/utils/connection-utils"; +import { AuthenticatorMeta } from "../meta/authenticator-meta"; +import { getConnectionDetails } from "../api/connections"; + +/** + * Interface for the route parameters. + */ +interface RouteParams { + tenantDomain?: string; + idpId?: string; +} + +/** + * Enum for test status. + */ +type TestStatus = "idle" | "pending" | "success" | "error"; + +/** + * Connection Test page component. + * + * @param props - Props injected to the component. + * @returns React element. + */ +const ConnectionTestPage: React.FC> = (props) => { + const { tenantDomain = "", idpId = "" } = props.match.params || {}; + + const { t } = useTranslation(); + + const [ connectionStatus, setConnectionStatus ] = useState("idle"); + const [ authStatus, setAuthStatus ] = useState("idle"); + const [ claimsStatus, setClaimsStatus ] = useState("idle"); + + /** + * Handles the "Run Tests" button click event. + * Executes GET request to test connection. + */ + const handleRunTests = async (): Promise => { + setConnectionStatus("pending"); + setAuthStatus("pending"); + setClaimsStatus("pending"); + + try { + // Dynamically import axios if not already imported + const axios = (await import("axios")).default; + // const response = await axios.post( + // `/api/server/v1/debug/connection/${idpId}`, + // { baseURL: window.location.origin } + // ); + const response = await axios.post( + `https://localhost:9443/api/server/v1/debug/connection/${idpId}`, + {}, + { withCredentials: true } + ); + // Adjust response parsing as per actual API response + // Save sessionId and idpId for later API calls + const sessionId = response.data.sessionId; + window.sessionStorage.setItem("idpDebugSessionId", sessionId); + window.sessionStorage.setItem("idpDebugIdpId", idpId); + window.sessionStorage.setItem("idpDebugTenantDomain", tenantDomain); + + // Open authorizationUrl in a new tab if present + if (response.data.authorizationUrl) { + const authPopup = window.open(response.data.authorizationUrl, "_blank"); + + // Monitor the popup and redirect when it closes (auth complete) + // This is the fallback if the backend doesn't redirect to debugSuccess.jsp + const checkPopupClosed = setInterval(() => { + try { + if (authPopup && authPopup.closed) { + clearInterval(checkPopupClosed); + const resultsUrl = `/t/${tenantDomain}/console/connections/${idpId}/debug-results#status=successful&sessionId=${encodeURIComponent(sessionId)}`; + history.push(resultsUrl); + } + } catch (e) { + } + }, 500); + + // Also set a timeout to navigate after 30 seconds in case popup.closed detection doesn't work + setTimeout(() => { + clearInterval(checkPopupClosed); + const resultsUrl = `/t/${tenantDomain}/console/connections/${idpId}/debug-results#status=successful&sessionId=${encodeURIComponent(sessionId)}`; + history.push(resultsUrl); + }, 30000); + } + + // Optionally update statuses based on response.status + if (response.data.status === "URL_GENERATED") { + setConnectionStatus("success"); + } else { + setConnectionStatus("error"); + } + // You may want to update authStatus/claimsStatus based on further API calls + } catch (error) { + setConnectionStatus("error"); + setAuthStatus("error"); + setClaimsStatus("error"); + } + }; + + /** + * Handles the back button click event. + */ + const handleBackButtonClick = (): void => { + // Navigate to the specific connection page + history.push(`/t/${tenantDomain}/console/connections/${idpId}`); + }; + + /** + * Renders the status icon based on the test status. + * + * @param status - The status of the test. + * @returns React element. + */ + const renderStatusIcon = (status: TestStatus) => { + switch (status) { + case "pending": + // Use spinner for "pending" (running) state + return ; + case "success": + return ; + case "error": + return ; + case "idle": + default: + // Use clock icon for "idle" state (matching wireframe's "Pending") + return ; + } + }; + + + // State for connector details + const [ connector, setConnector ] = useState(undefined); + const [ isConnectorDetailsFetchRequestLoading, setConnectorDetailFetchRequestLoading ] = useState(false); + // Get UIConfig and connectionResourcesUrl like in edit page + const { UIConfig } = useUIConfig(); + const connectionResourcesUrl = UIConfig?.connectionResourcesUrl; + const idpDescElement = useRef(null); + const testId = "idp-test-connection"; + + // Fetch connector details on mount + React.useEffect(() => { + if (!idpId) return; + setConnectorDetailFetchRequestLoading(true); + (async () => { + try { + const response = await getConnectionDetails(idpId); + setConnector(response); + } catch (error) { + setConnector(undefined); + } finally { + setConnectorDetailFetchRequestLoading(false); + } + })(); + }, [idpId]); + + const resolveConnectorName = (conn) => { + if (!conn) { + return "Connection Test"; + } + + if (ConnectionsManagementUtils.isConnectorIdentityProvider(conn)) { + return conn.name; + } + + const name = conn.friendlyName || conn.displayName || conn.name || "Connection Test"; + + return name; + }; + + const resolveConnectorImage = (conn) => { + const isOrganizationSSOIDP: boolean = ConnectionsManagementUtils.isOrganizationSSOConnection( + conn?.federatedAuthenticators?.defaultAuthenticatorId + ); + + if (!conn) { + return ; + } + + if (ConnectionsManagementUtils.isConnectorIdentityProvider(conn) && !isOrganizationSSOIDP) { + if (conn.image) { + const resolvedPath = ConnectionsManagementUtils.resolveConnectionResourcePath( + connectionResourcesUrl, + conn?.image + ); + return ( + + ); + } + return ; + } + return ( + + ); + }; + + const resolveConnectorDescription = (conn) => { + if (!conn) { + return "Test Federated IdP connection"; + } + return `Test the ${conn?.name || "connection"} connection`; + }; + + return ( + +
+ + + Run Tests + +
+ +
+ { t("console:develop.pages.idpTest.statusHeader", "Test Status") } +
+ + + + { renderStatusIcon(connectionStatus) } + + + + { t("console:develop.pages.idpTest.connectionCreation", "Connection Creation") } + + + + + + { renderStatusIcon(authStatus) } + + + + { t("console:develop.pages.idpTest.authentication", "Authentication") } + + + + + + { renderStatusIcon(claimsStatus) } + + + + { t("console:develop.pages.idpTest.claimsMapping", "Claims Mapping") } + + + + +
+
+ ); +}; + +/** + * A default export was added to support React.lazy. + * TODO: Change this to a named export once react starts supporting named exports for code splitting. + * @see {@link https://reactjs.org/docs/code-splitting.html#reactlazy} + */ +export default ConnectionTestPage; From 1cda8286a4d2125f9897cfa256382c04facee926 Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Sun, 2 Nov 2025 12:21:05 +0530 Subject: [PATCH 04/26] Add Identity Provider connection test and result pages --- apps/console/src/configs/routes.tsx | 32 +- .../pages/connection-test-result.tsx | 350 ++++++++++++++++++ 2 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 features/admin.connections.v1/pages/connection-test-result.tsx diff --git a/apps/console/src/configs/routes.tsx b/apps/console/src/configs/routes.tsx index 7bfb78ee58d..7e4e96f3099 100644 --- a/apps/console/src/configs/routes.tsx +++ b/apps/console/src/configs/routes.tsx @@ -585,6 +585,36 @@ export const getAppViewRoutes = (): RouteInterface[] => { protected: true, showOnSidePanel: false }, + { + component: lazy(() => + import("@wso2is/admin.connections.v1/pages/connection-test") + ), + exact: true, + icon: { + icon: getSidePanelIcons().childIcon + }, + id: "identityProvidersTest", + name: "Identity Providers Test", + // Direct path, since AppConstants does not have a test path + path: "/t/:tenantDomain/console/connections/:idpId/test", + protected: true, + showOnSidePanel: false + }, + { + component: lazy(() => + import("@wso2is/admin.connections.v1/pages/connection-test-result") + ), + exact: true, + icon: { + icon: getSidePanelIcons().childIcon + }, + id: "identityProvidersTestResult", + name: "Identity Providers Test Result", + // Direct path for debug results + path: "/t/:tenantDomain/console/connections/:idpId/debug-results", + protected: true, + showOnSidePanel: false + }, { component: lazy(() => import("@wso2is/admin.connections.v1/pages/connection-edit") @@ -615,7 +645,7 @@ export const getAppViewRoutes = (): RouteInterface[] => { } ], component: lazy(() => import("@wso2is/admin.connections.v1/pages/connections")), - exact: true, + exact: false, icon: { icon: }, diff --git a/features/admin.connections.v1/pages/connection-test-result.tsx b/features/admin.connections.v1/pages/connection-test-result.tsx new file mode 100644 index 00000000000..ad01bdab696 --- /dev/null +++ b/features/admin.connections.v1/pages/connection-test-result.tsx @@ -0,0 +1,350 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Debug Result page for Identity Provider connection tests. + */ + +import React, { useEffect, useState } from "react"; +import { RouteComponentProps } from "react-router-dom"; +import { TabPageLayout, PrimaryButton } from "@wso2is/react-components"; +import { Icon, Segment, Header, List, Tab } from "semantic-ui-react"; +import { useTranslation } from "react-i18next"; +import { history } from "@wso2is/admin.core.v1/helpers/history"; + +interface RouteParams { + tenantDomain?: string; + idpId?: string; +} + +type TestStatus = "idle" | "pending" | "success" | "error"; + +const DebugResultPage: React.FC> = (props) => { + const { tenantDomain = "", idpId = "" } = props.match.params || {}; + const { t } = useTranslation(); + + const [ loading, setLoading ] = useState(false); + const [ error, setError ] = useState(null); + const [ result, setResult ] = useState(null); + const [ sessionId, setSessionId ] = useState(() => { + try { + return window.sessionStorage.getItem("idpDebugSessionId"); + } catch (e) { + return null; + } + }); + const [ connectionStatus, setConnectionStatus ] = useState("idle"); + const [ authStatus, setAuthStatus ] = useState("idle"); + const [ claimsStatus, setClaimsStatus ] = useState("idle"); + const [ activeTab, setActiveTab ] = useState(0); + + // Try to resolve a session id from multiple places (sessionStorage, URL, opener) + useEffect(() => { + if (sessionId) { + return; + } + + // 1. Try URL query params / hash + try { + const urlSearch = new URLSearchParams(window.location.search); + const s1 = urlSearch.get("sessionId") || urlSearch.get("sessionid") || urlSearch.get("sid"); + if (s1) { + setSessionId(s1); + return; + } + + // Check hash fragment (e.g., #sessionId=...) + if (window.location.hash) { + const hash = window.location.hash.replace(/^#/, ""); + const hashParams = new URLSearchParams(hash); + const s2 = hashParams.get("sessionId") || hashParams.get("sessionid") || hashParams.get("sid"); + if (s2) { + setSessionId(s2); + return; + } + } + } catch (e) { + // ignore + } + + // 2. Try opener's sessionStorage if accessible (popup flow) + try { + if (window.opener && !window.opener.closed) { + try { + const openerSessionId = window.opener.sessionStorage.getItem("idpDebugSessionId"); + if (openerSessionId) { + setSessionId(openerSessionId); + return; + } + } catch (e) { + // cross-origin or inaccessible + } + } + } catch (e) { + // ignore + } + + // leave sessionId null if not found + }, [sessionId]); + + // Update test statuses based on result metadata + useEffect(() => { + if (result && result.metadata) { + const metadata = result.metadata; + setConnectionStatus(metadata.step_connection_status === "success" ? "success" : metadata.step_connection_status === "pending" ? "pending" : "error"); + setAuthStatus(metadata.step_authentication_status === "success" ? "success" : metadata.step_authentication_status === "pending" ? "pending" : "error"); + setClaimsStatus(metadata.step_claim_mapping_status === "success" ? "success" : metadata.step_claim_mapping_status === "pending" ? "pending" : "error"); + } + }, [result]); + + const fetchResult = async (sid?: string) => { + const effectiveSid = sid || sessionId; + if (!effectiveSid) { + setError("No debug session id found. Please run the connection test first."); + return; + } + + setLoading(true); + setError(null); + setResult(null); + try { + const axios = (await import("axios")).default; + const response = await axios.get( + `https://localhost:9443/api/server/v1/debug/result/${effectiveSid}`, + { withCredentials: true } + ); + setResult(response.data); + // eslint-disable-next-line no-console + console.log("[DebugResult] Successfully fetched results:", response.data); + } catch (err: any) { + // eslint-disable-next-line no-console + console.error("[DebugResult] Fetch error:", err?.response?.status, err?.message); + + // If 404, backend might still be processing - show a helpful message + if (err?.response?.status === 404) { + setError("Debug results not yet available. The backend may still be processing the authentication flow. Try refreshing in a few seconds."); + } else { + setError(err?.response?.data?.message || err?.message || t("console:develop.pages.idpTestResult.fetchError", "Failed to fetch debug results.")); + } + } finally { + setLoading(false); + } + }; + + useEffect(() => { + // Auto-fetch when we have a session id + if (sessionId) { + fetchResult(sessionId); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sessionId]); + + const renderStatusIcon = (status: TestStatus) => { + switch (status) { + case "pending": + return ; + case "success": + return ; + case "error": + return ; + case "idle": + default: + return ; + } + }; + + const handleBack = (): void => { + history.push(`/t/${tenantDomain}/console/connections/${idpId}`); + }; + + const tabPanes = [ + { + menuItem: "General Info", + render: () => ( + +
+                        {JSON.stringify({
+                            sessionId: result?.sessionId,
+                            idpName: result?.idpName,
+                            authenticator: result?.authenticator,
+                            username: result?.username,
+                            userId: result?.userId,
+                            success: result?.success,
+                            error: result?.error,
+                            timestamp: result?.timestamp
+                        }, null, 2)}
+                    
+
+ ) + }, + { + menuItem: "Claims Mapping", + render: () => ( + +
+
Incoming Claims
+
+                            {JSON.stringify(result?.incomingClaims, null, 2)}
+                        
+
Mapped Claims
+
+                            {JSON.stringify(result?.mappedClaims, null, 2)}
+                        
+
User Attributes
+
+                            {JSON.stringify(result?.userAttributes, null, 2)}
+                        
+
+
+ ) + }, + { + menuItem: "Logs", + render: () => ( + +
+                        {JSON.stringify(result, null, 2)}
+                    
+
+ ) + } + ]; + + return ( + + {/* Test Status Section */} + +
+ { t("console:develop.pages.idpTest.statusHeader", "Test Status") } +
+ + + + { renderStatusIcon(connectionStatus) } + + + + { t("console:develop.pages.idpTest.connectionCreation", "Connection Creation") } + + + + + + { renderStatusIcon(authStatus) } + + + + { t("console:develop.pages.idpTest.authentication", "Authentication") } + + + + + + { renderStatusIcon(claimsStatus) } + + + + { t("console:develop.pages.idpTest.claimsMapping", "Claims Mapping") } + + + + +
+ + {/* Results Section */} + { error && ( + + { error } +
+ fetchResult() } data-testid="idp-test-result-retry"> + { t("console:develop.pages.idpTestResult.retry", "Retry") } + +
+
+ ) } + + { !error && !sessionId && !loading && ( + + { t( + "console:develop.pages.idpTestResult.noSession", + "No debug session found. This page is usually opened after running the connection test." + ) } + + ) } + + { !error && result && ( + + setActiveTab(data.activeIndex as number) } /> + + ) } + + { !error && !result && sessionId && !loading && ( + + { t("console:develop.pages.idpTestResult.noResult", "No results available.") } +
+ fetchResult() } data-testid="idp-test-result-refresh"> + { t("console:develop.pages.idpTestResult.refresh", "Refresh") } + +
+
+ ) } +
+ ); +}; + +export default DebugResultPage; From 673a5456f717fac2eb80f84ec5a5dc99ee41be6c Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Tue, 4 Nov 2025 09:33:25 +0530 Subject: [PATCH 05/26] Refactor debugSuccess page --- .../src/main/webapp/debugSuccess.jsp | 148 +++++++++--------- 1 file changed, 72 insertions(+), 76 deletions(-) diff --git a/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp index 7d4f8a11173..503272fb8db 100644 --- a/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp +++ b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp @@ -6,7 +6,7 @@ ~ in compliance with the License. ~ You may obtain a copy of the License at ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, ~ software distributed under the License is distributed on an @@ -17,20 +17,30 @@ --%> <%@ page import="java.io.File" %> +<%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.AuthenticationEndpointUtil" %> +<%@ page import="org.owasp.encoder.Encode" %> +<%@ page import="org.apache.commons.text.StringEscapeUtils" %> +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> +<%@ include file="includes/localize.jsp" %> -<%@include file="includes/localize.jsp" %> +<%-- Include tenant context --%> <%-- Branding Preferences --%> +<% request.setAttribute("pageName","debug-success"); %> + +<%-- Data for the layout from the page --%> <% - request.setAttribute("pageName", "authentication-success"); + layoutData.put("isResponsePage", true); + layoutData.put("isSuccessResponse", true); + layoutData.put("isDebugSuccessPage", true); %> - + <%-- header --%> <% @@ -42,79 +52,65 @@ <% } %> -"> - - - <%-- product-title --%> - <% - File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); - if (productTitleFile.exists()) { - %> - - <% } else { %> - - <% } %> - - -
-

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "Authentication successful")%>

-

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "You will be redirected shortly")%>...

-
-
- - <%-- product-footer --%> - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> - - - - -
- -<%-- footer --%> -<% - File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); - if (footerFile.exists()) { -%> - -<% } else { %> - -<% } %> - - + <%-- footer --%> + <% + File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { + %> + + <% } else { %> + + <% } %> + From 105d5a82731a8999a4108770d9246c3ef4a2587f Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Thu, 6 Nov 2025 11:49:55 +0530 Subject: [PATCH 06/26] Add minor improvements --- .../pages/connection-test-result.tsx | 67 ++++++++++++------- .../pages/connection-test.tsx | 2 +- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/features/admin.connections.v1/pages/connection-test-result.tsx b/features/admin.connections.v1/pages/connection-test-result.tsx index ad01bdab696..f3f2c936049 100644 --- a/features/admin.connections.v1/pages/connection-test-result.tsx +++ b/features/admin.connections.v1/pages/connection-test-result.tsx @@ -40,6 +40,7 @@ const DebugResultPage: React.FC> = (props) => { const [ loading, setLoading ] = useState(false); const [ error, setError ] = useState(null); const [ result, setResult ] = useState(null); + const [ isFetched, setIsFetched ] = useState(false); const [ sessionId, setSessionId ] = useState(() => { try { return window.sessionStorage.getItem("idpDebugSessionId"); @@ -111,16 +112,16 @@ const DebugResultPage: React.FC> = (props) => { } }, [result]); - const fetchResult = async (sid?: string) => { + const fetchResult = async (sid?: string, retryCount: number = 0) => { const effectiveSid = sid || sessionId; if (!effectiveSid) { - setError("No debug session id found. Please run the connection test first."); + setError("No debug session id found..."); return; } - setLoading(true); - setError(null); - setResult(null); + setLoading(true); + setError(null); + setResult(null); try { const axios = (await import("axios")).default; const response = await axios.get( @@ -128,13 +129,25 @@ const DebugResultPage: React.FC> = (props) => { { withCredentials: true } ); setResult(response.data); + setIsFetched(true); // eslint-disable-next-line no-console console.log("[DebugResult] Successfully fetched results:", response.data); } catch (err: any) { // eslint-disable-next-line no-console console.error("[DebugResult] Fetch error:", err?.response?.status, err?.message); - // If 404, backend might still be processing - show a helpful message + // If 404, backend might still be processing - auto-retry up to 2 times + if (err?.response?.status === 404 && retryCount < 2) { + // eslint-disable-next-line no-console + console.log(`[DebugResult] Got 404, retrying... (attempt ${retryCount + 1}/2)`); + setError("Backend is processing results, retrying in 2 seconds..."); + setLoading(false); + setTimeout(() => { + fetchResult(effectiveSid, retryCount + 1); + }, 2000); + return; + } + if (err?.response?.status === 404) { setError("Debug results not yet available. The backend may still be processing the authentication flow. Try refreshing in a few seconds."); } else { @@ -146,12 +159,12 @@ const DebugResultPage: React.FC> = (props) => { }; useEffect(() => { - // Auto-fetch when we have a session id - if (sessionId) { + // Auto-fetch when we have a session id and not already fetched + if (sessionId && !isFetched && !error) { fetchResult(sessionId); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sessionId]); + }, [sessionId, isFetched, error]); const renderStatusIcon = (status: TestStatus) => { switch (status) { @@ -168,26 +181,28 @@ const DebugResultPage: React.FC> = (props) => { }; const handleBack = (): void => { - history.push(`/t/${tenantDomain}/console/connections/${idpId}`); + history.push(`/t/${tenantDomain}/console/connections/${idpId}/test`); }; const tabPanes = [ { - menuItem: "General Info", + menuItem: "ID Token", render: () => ( -
-                        {JSON.stringify({
-                            sessionId: result?.sessionId,
-                            idpName: result?.idpName,
-                            authenticator: result?.authenticator,
-                            username: result?.username,
-                            userId: result?.userId,
-                            success: result?.success,
-                            error: result?.error,
-                            timestamp: result?.timestamp
-                        }, null, 2)}
-                    
+
+
ID Token
+
+                            {JSON.stringify(result?.idToken, null, 2)}
+                        
+
IDP Name
+
+                            {JSON.stringify(result?.idpName, null, 2)}
+                        
+
External Redirect URL
+
+                            {JSON.stringify(result?.externalRedirectUrl, null, 2)}
+                        
+
) }, @@ -217,7 +232,9 @@ const DebugResultPage: React.FC> = (props) => { render: () => (
-                        {JSON.stringify(result, null, 2)}
+                        {JSON.stringify({
+                            metadata: result?.metadata,
+                        }, null, 2)}
                     
) @@ -233,7 +250,7 @@ const DebugResultPage: React.FC> = (props) => { backButton={{ onClick: handleBack, "data-testid": "idp-test-result-back-button", - text: t("console:develop.pages.idpTest.backButton", "Go back to Connection") + text: t("console:develop.pages.idpTest.backButton", "Go back to Test") }} > {/* Test Status Section */} diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index 0aa198a4b23..7f2639e76d7 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -282,7 +282,7 @@ const ConnectionTestPage: React.FC> = (props) = basic padded="very" className="bordered emphasized" - style={{ marginTop: "3rem" }} + style={{ marginTop: "3rem", maxWidth: "800px"}} data-componentid="emphasized-segment" data-testid="emphasized-segment" > From ed6b2edecc44a87ed0eafc23dba1f8c9d1a4b70b Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Tue, 25 Nov 2025 14:41:55 +0530 Subject: [PATCH 07/26] Remove Debug Result page and update Connection Test page UI --- apps/console/src/configs/routes.tsx | 15 - .../pages/connection-edit.tsx | 2 +- .../pages/connection-test-result.tsx | 367 ---------- .../pages/connection-test.tsx | 641 +++++++++++++++--- 4 files changed, 547 insertions(+), 478 deletions(-) delete mode 100644 features/admin.connections.v1/pages/connection-test-result.tsx diff --git a/apps/console/src/configs/routes.tsx b/apps/console/src/configs/routes.tsx index 7e4e96f3099..071914629fc 100644 --- a/apps/console/src/configs/routes.tsx +++ b/apps/console/src/configs/routes.tsx @@ -600,21 +600,6 @@ export const getAppViewRoutes = (): RouteInterface[] => { protected: true, showOnSidePanel: false }, - { - component: lazy(() => - import("@wso2is/admin.connections.v1/pages/connection-test-result") - ), - exact: true, - icon: { - icon: getSidePanelIcons().childIcon - }, - id: "identityProvidersTestResult", - name: "Identity Providers Test Result", - // Direct path for debug results - path: "/t/:tenantDomain/console/connections/:idpId/debug-results", - protected: true, - showOnSidePanel: false - }, { component: lazy(() => import("@wso2is/admin.connections.v1/pages/connection-edit") diff --git a/features/admin.connections.v1/pages/connection-edit.tsx b/features/admin.connections.v1/pages/connection-edit.tsx index cfe3e8ecbda..3fa6fcc805b 100644 --- a/features/admin.connections.v1/pages/connection-edit.tsx +++ b/features/admin.connections.v1/pages/connection-edit.tsx @@ -715,7 +715,7 @@ const ConnectionEditPage: FunctionComponent = history.push(`/t/${tenantDomain}/console/connections/${idpId}/test`); }} > - + Test Connection ) } diff --git a/features/admin.connections.v1/pages/connection-test-result.tsx b/features/admin.connections.v1/pages/connection-test-result.tsx deleted file mode 100644 index f3f2c936049..00000000000 --- a/features/admin.connections.v1/pages/connection-test-result.tsx +++ /dev/null @@ -1,367 +0,0 @@ -/** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - * - * WSO2 LLC. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Debug Result page for Identity Provider connection tests. - */ - -import React, { useEffect, useState } from "react"; -import { RouteComponentProps } from "react-router-dom"; -import { TabPageLayout, PrimaryButton } from "@wso2is/react-components"; -import { Icon, Segment, Header, List, Tab } from "semantic-ui-react"; -import { useTranslation } from "react-i18next"; -import { history } from "@wso2is/admin.core.v1/helpers/history"; - -interface RouteParams { - tenantDomain?: string; - idpId?: string; -} - -type TestStatus = "idle" | "pending" | "success" | "error"; - -const DebugResultPage: React.FC> = (props) => { - const { tenantDomain = "", idpId = "" } = props.match.params || {}; - const { t } = useTranslation(); - - const [ loading, setLoading ] = useState(false); - const [ error, setError ] = useState(null); - const [ result, setResult ] = useState(null); - const [ isFetched, setIsFetched ] = useState(false); - const [ sessionId, setSessionId ] = useState(() => { - try { - return window.sessionStorage.getItem("idpDebugSessionId"); - } catch (e) { - return null; - } - }); - const [ connectionStatus, setConnectionStatus ] = useState("idle"); - const [ authStatus, setAuthStatus ] = useState("idle"); - const [ claimsStatus, setClaimsStatus ] = useState("idle"); - const [ activeTab, setActiveTab ] = useState(0); - - // Try to resolve a session id from multiple places (sessionStorage, URL, opener) - useEffect(() => { - if (sessionId) { - return; - } - - // 1. Try URL query params / hash - try { - const urlSearch = new URLSearchParams(window.location.search); - const s1 = urlSearch.get("sessionId") || urlSearch.get("sessionid") || urlSearch.get("sid"); - if (s1) { - setSessionId(s1); - return; - } - - // Check hash fragment (e.g., #sessionId=...) - if (window.location.hash) { - const hash = window.location.hash.replace(/^#/, ""); - const hashParams = new URLSearchParams(hash); - const s2 = hashParams.get("sessionId") || hashParams.get("sessionid") || hashParams.get("sid"); - if (s2) { - setSessionId(s2); - return; - } - } - } catch (e) { - // ignore - } - - // 2. Try opener's sessionStorage if accessible (popup flow) - try { - if (window.opener && !window.opener.closed) { - try { - const openerSessionId = window.opener.sessionStorage.getItem("idpDebugSessionId"); - if (openerSessionId) { - setSessionId(openerSessionId); - return; - } - } catch (e) { - // cross-origin or inaccessible - } - } - } catch (e) { - // ignore - } - - // leave sessionId null if not found - }, [sessionId]); - - // Update test statuses based on result metadata - useEffect(() => { - if (result && result.metadata) { - const metadata = result.metadata; - setConnectionStatus(metadata.step_connection_status === "success" ? "success" : metadata.step_connection_status === "pending" ? "pending" : "error"); - setAuthStatus(metadata.step_authentication_status === "success" ? "success" : metadata.step_authentication_status === "pending" ? "pending" : "error"); - setClaimsStatus(metadata.step_claim_mapping_status === "success" ? "success" : metadata.step_claim_mapping_status === "pending" ? "pending" : "error"); - } - }, [result]); - - const fetchResult = async (sid?: string, retryCount: number = 0) => { - const effectiveSid = sid || sessionId; - if (!effectiveSid) { - setError("No debug session id found..."); - return; - } - - setLoading(true); - setError(null); - setResult(null); - try { - const axios = (await import("axios")).default; - const response = await axios.get( - `https://localhost:9443/api/server/v1/debug/result/${effectiveSid}`, - { withCredentials: true } - ); - setResult(response.data); - setIsFetched(true); - // eslint-disable-next-line no-console - console.log("[DebugResult] Successfully fetched results:", response.data); - } catch (err: any) { - // eslint-disable-next-line no-console - console.error("[DebugResult] Fetch error:", err?.response?.status, err?.message); - - // If 404, backend might still be processing - auto-retry up to 2 times - if (err?.response?.status === 404 && retryCount < 2) { - // eslint-disable-next-line no-console - console.log(`[DebugResult] Got 404, retrying... (attempt ${retryCount + 1}/2)`); - setError("Backend is processing results, retrying in 2 seconds..."); - setLoading(false); - setTimeout(() => { - fetchResult(effectiveSid, retryCount + 1); - }, 2000); - return; - } - - if (err?.response?.status === 404) { - setError("Debug results not yet available. The backend may still be processing the authentication flow. Try refreshing in a few seconds."); - } else { - setError(err?.response?.data?.message || err?.message || t("console:develop.pages.idpTestResult.fetchError", "Failed to fetch debug results.")); - } - } finally { - setLoading(false); - } - }; - - useEffect(() => { - // Auto-fetch when we have a session id and not already fetched - if (sessionId && !isFetched && !error) { - fetchResult(sessionId); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sessionId, isFetched, error]); - - const renderStatusIcon = (status: TestStatus) => { - switch (status) { - case "pending": - return ; - case "success": - return ; - case "error": - return ; - case "idle": - default: - return ; - } - }; - - const handleBack = (): void => { - history.push(`/t/${tenantDomain}/console/connections/${idpId}/test`); - }; - - const tabPanes = [ - { - menuItem: "ID Token", - render: () => ( - -
-
ID Token
-
-                            {JSON.stringify(result?.idToken, null, 2)}
-                        
-
IDP Name
-
-                            {JSON.stringify(result?.idpName, null, 2)}
-                        
-
External Redirect URL
-
-                            {JSON.stringify(result?.externalRedirectUrl, null, 2)}
-                        
-
-
- ) - }, - { - menuItem: "Claims Mapping", - render: () => ( - -
-
Incoming Claims
-
-                            {JSON.stringify(result?.incomingClaims, null, 2)}
-                        
-
Mapped Claims
-
-                            {JSON.stringify(result?.mappedClaims, null, 2)}
-                        
-
User Attributes
-
-                            {JSON.stringify(result?.userAttributes, null, 2)}
-                        
-
-
- ) - }, - { - menuItem: "Logs", - render: () => ( - -
-                        {JSON.stringify({
-                            metadata: result?.metadata,
-                        }, null, 2)}
-                    
-
- ) - } - ]; - - return ( - - {/* Test Status Section */} - -
- { t("console:develop.pages.idpTest.statusHeader", "Test Status") } -
- - - - { renderStatusIcon(connectionStatus) } - - - - { t("console:develop.pages.idpTest.connectionCreation", "Connection Creation") } - - - - - - { renderStatusIcon(authStatus) } - - - - { t("console:develop.pages.idpTest.authentication", "Authentication") } - - - - - - { renderStatusIcon(claimsStatus) } - - - - { t("console:develop.pages.idpTest.claimsMapping", "Claims Mapping") } - - - - -
- - {/* Results Section */} - { error && ( - - { error } -
- fetchResult() } data-testid="idp-test-result-retry"> - { t("console:develop.pages.idpTestResult.retry", "Retry") } - -
-
- ) } - - { !error && !sessionId && !loading && ( - - { t( - "console:develop.pages.idpTestResult.noSession", - "No debug session found. This page is usually opened after running the connection test." - ) } - - ) } - - { !error && result && ( - - setActiveTab(data.activeIndex as number) } /> - - ) } - - { !error && !result && sessionId && !loading && ( - - { t("console:develop.pages.idpTestResult.noResult", "No results available.") } -
- fetchResult() } data-testid="idp-test-result-refresh"> - { t("console:develop.pages.idpTestResult.refresh", "Refresh") } - -
-
- ) } -
- ); -}; - -export default DebugResultPage; diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index 7f2639e76d7..94dab77fff5 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -28,14 +28,15 @@ import { AppAvatar, AnimatedAvatar } from "@wso2is/react-components"; -import React, { useState, useRef, Fragment } from "react"; +import React, { useState, useRef, Fragment, useEffect } from "react"; import useUIConfig from "@wso2is/admin.core.v1/hooks/use-ui-configs"; import { useTranslation } from "react-i18next"; import { RouteComponentProps } from "react-router-dom"; -import { Header, Icon, List, Segment } from "semantic-ui-react"; +import { Header, Icon, List, Segment, Tab } from "semantic-ui-react"; import { ConnectionsManagementUtils } from "@wso2is/admin.connections.v1/utils/connection-utils"; import { AuthenticatorMeta } from "../meta/authenticator-meta"; import { getConnectionDetails } from "../api/connections"; +import { Code } from "@wso2is/react-components"; /** * Interface for the route parameters. @@ -64,80 +65,174 @@ const ConnectionTestPage: React.FC> = (props) = const [ connectionStatus, setConnectionStatus ] = useState("idle"); const [ authStatus, setAuthStatus ] = useState("idle"); const [ claimsStatus, setClaimsStatus ] = useState("idle"); + const [ debugId, setDebugId ] = useState(null); + const [ result, setResult ] = useState(null); + const [ error, setError ] = useState(null); + const [ loading, setLoading ] = useState(false); + const [ hasError, setHasError ] = useState(false); + const [ activeTab, setActiveTab ] = useState(0); + const [ showResults, setShowResults ] = useState(false); + + /** + * Fetches the test results from the backend. + */ + const fetchResult = async (sid: string): Promise => { + if (!sid) { + setError("No debug session id found..."); + return; + } + + setLoading(true); + setError(null); + setResult(null); + + try { + const axios = (await import("axios")).default; + const response = await axios.get( + `https://localhost:9443/api/server/v1/debug/result/${sid}`, + { withCredentials: true } + ); + setResult(response.data); + setShowResults(true); + // eslint-disable-next-line no-console + console.log("[ConnectionTest] Successfully fetched results:", response.data); + } catch (err: any) { + // eslint-disable-next-line no-console + console.error("[ConnectionTest] Fetch error:", err?.response?.status, err?.message); + + if (err?.response?.status === 404) { + setError("Something went wrong."); + } else { + setError(err?.response?.data?.message || err?.message || "Failed to fetch debug results."); + } + } finally { + setLoading(false); + } + }; /** * Handles the "Run Tests" button click event. - * Executes GET request to test connection. + * Executes POST request to test connection. */ const handleRunTests = async (): Promise => { setConnectionStatus("pending"); setAuthStatus("pending"); setClaimsStatus("pending"); + setError(null); + setResult(null); + setShowResults(false); try { - // Dynamically import axios if not already imported const axios = (await import("axios")).default; - // const response = await axios.post( - // `/api/server/v1/debug/connection/${idpId}`, - // { baseURL: window.location.origin } - // ); + const payload = { + resourceId: idpId, + resourceType: "IDP", + properties: {} + }; + const response = await axios.post( - `https://localhost:9443/api/server/v1/debug/connection/${idpId}`, - {}, + `https://localhost:9443/api/server/v1/debug/`, + payload, { withCredentials: true } ); - // Adjust response parsing as per actual API response - // Save sessionId and idpId for later API calls - const sessionId = response.data.sessionId; - window.sessionStorage.setItem("idpDebugSessionId", sessionId); - window.sessionStorage.setItem("idpDebugIdpId", idpId); - window.sessionStorage.setItem("idpDebugTenantDomain", tenantDomain); + + const newDebugId = response.data.result.debugId; + const authorizationUrl = response.data.result.authorizationUrl; + const status = response.data.result.status; + + setDebugId(newDebugId); // Open authorizationUrl in a new tab if present - if (response.data.authorizationUrl) { - const authPopup = window.open(response.data.authorizationUrl, "_blank"); + if (authorizationUrl) { + const authPopup = window.open(authorizationUrl, "_blank"); - // Monitor the popup and redirect when it closes (auth complete) - // This is the fallback if the backend doesn't redirect to debugSuccess.jsp + // Monitor the popup and fetch results when it closes const checkPopupClosed = setInterval(() => { try { if (authPopup && authPopup.closed) { clearInterval(checkPopupClosed); - const resultsUrl = `/t/${tenantDomain}/console/connections/${idpId}/debug-results#status=successful&sessionId=${encodeURIComponent(sessionId)}`; - history.push(resultsUrl); + // Fetch results after popup closes + setTimeout(() => { + fetchResult(newDebugId); + }, 1000); } } catch (e) { + // ignore } }, 500); - // Also set a timeout to navigate after 30 seconds in case popup.closed detection doesn't work + // Set a timeout to fetch results after 30 seconds in case popup detection doesn't work setTimeout(() => { clearInterval(checkPopupClosed); - const resultsUrl = `/t/${tenantDomain}/console/connections/${idpId}/debug-results#status=successful&sessionId=${encodeURIComponent(sessionId)}`; - history.push(resultsUrl); + fetchResult(newDebugId); }, 30000); + } else { + // If no authorizationUrl, fetch results immediately + setTimeout(() => { + fetchResult(newDebugId); + }, 2000); } - // Optionally update statuses based on response.status - if (response.data.status === "URL_GENERATED") { + // Update status based on response + if (status === "SUCCESS") { setConnectionStatus("success"); } else { setConnectionStatus("error"); } - // You may want to update authStatus/claimsStatus based on further API calls - } catch (error) { + } catch (error: any) { setConnectionStatus("error"); setAuthStatus("error"); setClaimsStatus("error"); + setError(error?.response?.data?.message || error?.message || "Failed to run test."); } }; + // Update test statuses based on result metadata + useEffect(() => { + if (result && result.metadata) { + const metadata = result.metadata; + setConnectionStatus(metadata.connectionStatus === "success" ? "success" : metadata.connectionStatus === "pending" ? "pending" : "error"); + setAuthStatus(metadata.authenticationStatus === "success" ? "success" : metadata.authenticationStatus === "pending" ? "pending" : "error"); + setClaimsStatus(metadata.claimMappingStatus === "success" ? "success" : metadata.claimMappingStatus === "pending" ? "pending" : "error"); + + // Check if any step explicitly failed (not pending or success) + const hasStepError = ( + (metadata.connectionStatus !== "success" && metadata.connectionStatus !== "pending") || + (metadata.authenticationStatus !== "success" && metadata.authenticationStatus !== "pending") || + (metadata.claimMappingStatus !== "success" && metadata.claimMappingStatus !== "pending") || + result?.error + ); + + if (hasStepError) { + setHasError(true); + setActiveTab(2); // Switch to Logs tab (index 2) + } else { + setHasError(false); + } + } + }, [result]); + /** * Handles the back button click event. */ const handleBackButtonClick = (): void => { - // Navigate to the specific connection page - history.push(`/t/${tenantDomain}/console/connections/${idpId}`); + // Navigate to the specific connection page + history.push(`/t/${tenantDomain}/console/connections/${idpId}`); + }; + + /** + * Handles the "Back to Test" button when viewing results. + */ + const handleBackToTest = (): void => { + setShowResults(false); + setResult(null); + setError(null); + setDebugId(null); + setConnectionStatus("idle"); + setAuthStatus("idle"); + setClaimsStatus("idle"); + setHasError(false); + setActiveTab(0); }; /** @@ -251,77 +346,433 @@ const ConnectionTestPage: React.FC> = (props) = return `Test the ${conn?.name || "connection"} connection`; }; + /** + * Renders the results tabs when test results are available. + */ + const renderResultsTabs = () => { + const tabPanes = [ + { + menuItem: "Token Details", + render: () => { + // Helper to decode JWT + const decodeJWT = (token) => { + if (!token || typeof token !== "string" || token.split(".").length < 3) return null; + const [header, payload, signature] = token.split("."); + const decode = (str) => { + try { + let base64 = str.replace(/-/g, "+").replace(/_/g, "/"); + while (base64.length % 4) base64 += "="; + return JSON.parse(atob(base64)); + } catch { + return null; + } + }; + return { + header: decode(header), + payload: decode(payload), + signature + }; + }; + + const idToken = result?.idToken; + const decoded = decodeJWT(idToken); + + return ( + +
+ {decoded ? ( +
+
ID Token
+
+
+
Header
+
+                                                    {JSON.stringify(decoded.header, null, 2)}
+                                                
+
+
+
Payload
+
+                                                    {JSON.stringify(decoded.payload, null, 2)}
+                                                
+
+
+
Signature
+
+
+                                                        {decoded.signature}
+                                                    
+
+
+
+
+ ) : ( +
+ Unable to decode token or not a valid JWT. +
+ )} +
External Redirect URL
+
+                                    {typeof result?.externalRedirectUrl === 'string' ? result.externalRedirectUrl : JSON.stringify(result?.externalRedirectUrl, null, 2)}
+                                
+
+
+ ); + } + }, + { + menuItem: "Claims Mappings", + render: () => { + const claimsArray = Array.isArray(result?.mappedClaims) ? result?.mappedClaims : []; + const sortedClaims = claimsArray.sort((a, b) => { + if (a.status === 'Successful' && b.status !== 'Successful') return -1; + if (a.status !== 'Successful' && b.status === 'Successful') return 1; + return 0; + }); + + const formatValue = (val) => { + if (val === null || val === undefined) return '-'; + if (typeof val === 'object') return JSON.stringify(val); + return String(val); + }; + + return ( + +
+
Claims Mapping Table
+
+ + + + + + + + + + + {sortedClaims.length === 0 && ( + + + + )} + {sortedClaims.map((claim, idx) => ( + + + + + + + ))} + +
IDP ClaimIS Claim (URI)ValueMapping Status
No claims found.
{claim.idpClaim}{claim.isClaim || '-'}{formatValue(claim.value)}{claim.status}
+
+
+
+ ); + } + }, + { + menuItem: "Logs", + render: () => { + const formatLogs = () => { + const metadata = result?.metadata || {}; + const errorDetails = result?.error_details || null; + const errorDescription = result?.error_description || null; + + return ( +
+ {(errorDetails || errorDescription) && ( +
+
Error Information
+
+ {errorDescription && ( +
+ Description: +

+ {errorDescription} +

+
+ )} + {errorDetails && ( +
+ Details: +

+ {errorDetails} +

+
+ )} +
+
+ )} + + {Object.keys(metadata).length > 0 && ( +
+
Steps
+
+ {[ + { key: 'connectionStatus', label: 'Connection Creation' }, + { key: 'authenticationStatus', label: 'Authentication' }, + { key: 'claimMappingStatus', label: 'Claims Mapping' } + ].map(({ key, label }) => ( + key in metadata ? ( +
+ {label}: + + {String(metadata[key])} + +
+ ) : null + ))} +
+
+ )} + + {!errorDetails && !errorDescription && Object.keys(metadata).length === 0 && ( +
+ No log information available. +
+ )} +
+ ); + }; + + return ( + +
+ {formatLogs()} +
+
+ ); + } + } + ]; + + return setActiveTab(data.activeIndex as number)} />; + }; + return ( + + Rerun Test + + ) : null + } titleTextAlign="left" - contentTopMargin={ true } - bottomMargin={ false } - data-testid={ `${testId}-page-layout` } + contentTopMargin={true} + bottomMargin={false} + data-testid={`${testId}-page-layout`} > -
- - - Run Tests - -
- -
- { t("console:develop.pages.idpTest.statusHeader", "Test Status") } -
- - - - { renderStatusIcon(connectionStatus) } - - - - { t("console:develop.pages.idpTest.connectionCreation", "Connection Creation") } - - - - - - { renderStatusIcon(authStatus) } - - - - { t("console:develop.pages.idpTest.authentication", "Authentication") } - - - - - - { renderStatusIcon(claimsStatus) } - - - - { t("console:develop.pages.idpTest.claimsMapping", "Claims Mapping") } - - - - -
+ {!showResults && ( + <> +
+ + + Run Tests + +
+ +
+ {t("console:develop.pages.idpTest.statusHeader", "Test Status")} +
+ + + + {renderStatusIcon(connectionStatus)} + + + + {t("console:develop.pages.idpTest.connectionCreation", "Connection Creation")} + + + + + + {renderStatusIcon(authStatus)} + + + + {t("console:develop.pages.idpTest.authentication", "Authentication")} + + + + + + {renderStatusIcon(claimsStatus)} + + + + {t("console:develop.pages.idpTest.claimsMapping", "Claims Mapping")} + + + + +
+ + )} + + {showResults && ( + <> + {/* Test Status Section */} + +
+ {t("console:develop.pages.idpTest.statusHeader", "Test Status")} +
+ {hasError && ( +
+

⚠️ Errors Detected

+

Check the details below about the failed steps.

+
+ )} + + + + {renderStatusIcon(connectionStatus)} + + + + {t("console:develop.pages.idpTest.connectionCreation", "Connection Creation")} + + + + + + {renderStatusIcon(authStatus)} + + + + {t("console:develop.pages.idpTest.authentication", "Authentication")} + + + + + + {renderStatusIcon(claimsStatus)} + + + + {t("console:develop.pages.idpTest.claimsMapping", "Claims Mapping")} + + + + +
+ + {/* Results Section */} + {error && ( + + {error} +
+ + Retry + +
+
+ )} + + {!error && result && ( + + {renderResultsTabs()} + + )} + + {!error && !result && debugId && !loading && ( + + No results available. +
+ fetchResult(debugId)} data-testid="idp-test-result-refresh"> + Refresh + +
+
+ )} + + )}
); }; From dab527032ff8473c8a23611082e2c615efa3a5c3 Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Tue, 27 Jan 2026 13:17:35 +0530 Subject: [PATCH 08/26] Add an empty placeholder --- .../pages/connection-test.tsx | 67 +++++-------------- 1 file changed, 18 insertions(+), 49 deletions(-) diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index 94dab77fff5..aad60a33dca 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -36,7 +36,9 @@ import { Header, Icon, List, Segment, Tab } from "semantic-ui-react"; import { ConnectionsManagementUtils } from "@wso2is/admin.connections.v1/utils/connection-utils"; import { AuthenticatorMeta } from "../meta/authenticator-meta"; import { getConnectionDetails } from "../api/connections"; -import { Code } from "@wso2is/react-components"; +import { EmptyPlaceholder } from "@wso2is/react-components"; +import { getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1/configs/ui"; + /** * Interface for the route parameters. @@ -612,64 +614,31 @@ const ConnectionTestPage: React.FC> = (props) = bottomMargin={false} data-testid={`${testId}-page-layout`} > - {!showResults && ( + { !showResults && ( <>
Run Tests
- -
- {t("console:develop.pages.idpTest.statusHeader", "Test Status")} -
- - - - {renderStatusIcon(connectionStatus)} - - - - {t("console:develop.pages.idpTest.connectionCreation", "Connection Creation")} - - - - - - {renderStatusIcon(authStatus)} - - - - {t("console:develop.pages.idpTest.authentication", "Authentication")} - - - - - - {renderStatusIcon(claimsStatus)} - - - - {t("console:develop.pages.idpTest.claimsMapping", "Claims Mapping")} - - - - -
+ - )} + ) } + {showResults && ( <> From 02519ed9e91f4ab8da9037682c01e9b5561f11d0 Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Tue, 27 Jan 2026 13:18:52 +0530 Subject: [PATCH 09/26] Add condition to render test button for identity providers --- features/admin.connections.v1/pages/connection-edit.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/admin.connections.v1/pages/connection-edit.tsx b/features/admin.connections.v1/pages/connection-edit.tsx index 3fa6fcc805b..2a1ec5bbd2e 100644 --- a/features/admin.connections.v1/pages/connection-edit.tsx +++ b/features/admin.connections.v1/pages/connection-edit.tsx @@ -700,7 +700,7 @@ const ConnectionEditPage: FunctionComponent = imageType: "square" } } title={ resolveConnectorName(connector) } - action={ ( + action={ ConnectionsManagementUtils.isConnectorIdentityProvider(connector) ? ( = Test Connection - ) } + ) : null } contentTopMargin={ true } description={ resolveConnectorDescription(connector) } image={ resolveConnectorImage(connector) } From b1c370d06268b5d0af71c827b859bca6ddf2f0ec Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Tue, 27 Jan 2026 14:13:48 +0530 Subject: [PATCH 10/26] Add debug endpoints to connection resource --- .../admin.connections.v1/configs/endpoints.ts | 4 +- .../admin.connections.v1/models/endpoints.ts | 2 + .../pages/connection-test.tsx | 54 ++- .../admin.core.v1/store/reducers/config.ts | 343 ++++++++++++++---- 4 files changed, 327 insertions(+), 76 deletions(-) diff --git a/features/admin.connections.v1/configs/endpoints.ts b/features/admin.connections.v1/configs/endpoints.ts index a4484863472..a22f2532781 100644 --- a/features/admin.connections.v1/configs/endpoints.ts +++ b/features/admin.connections.v1/configs/endpoints.ts @@ -37,6 +37,8 @@ export const getConnectionResourceEndpoints = (serverHost: string): ConnectionRe localAuthenticators: `${ serverHost }/api/server/v1/configs/authenticators`, multiFactorAuthenticators: `${ serverHost }/api/server/v1/identity-governance/${ CommonAuthenticatorConstants.MFA_CONNECTOR_CATEGORY_ID - }` + }`, + debug: `${ serverHost }/api/server/v1/debug`, + debugResult: `${ serverHost }/api/server/v1/debug/result` }; }; diff --git a/features/admin.connections.v1/models/endpoints.ts b/features/admin.connections.v1/models/endpoints.ts index 39740a60a0e..a7cf33265a2 100644 --- a/features/admin.connections.v1/models/endpoints.ts +++ b/features/admin.connections.v1/models/endpoints.ts @@ -28,4 +28,6 @@ export interface ConnectionResourceEndpointsInterface { identityProviders: string; localAuthenticators: string; multiFactorAuthenticators: string; + debug: string; + debugResult: string; } diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index aad60a33dca..eda003f4cd9 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -30,6 +30,8 @@ import { } from "@wso2is/react-components"; import React, { useState, useRef, Fragment, useEffect } from "react"; import useUIConfig from "@wso2is/admin.core.v1/hooks/use-ui-configs"; +import useResourceEndpoints from "@wso2is/admin.core.v1/hooks/use-resource-endpoints"; + import { useTranslation } from "react-i18next"; import { RouteComponentProps } from "react-router-dom"; import { Header, Icon, List, Segment, Tab } from "semantic-ui-react"; @@ -63,6 +65,8 @@ const ConnectionTestPage: React.FC> = (props) = const { tenantDomain = "", idpId = "" } = props.match.params || {}; const { t } = useTranslation(); + const { resourceEndpoints } = useResourceEndpoints(); + const [ connectionStatus, setConnectionStatus ] = useState("idle"); const [ authStatus, setAuthStatus ] = useState("idle"); @@ -75,6 +79,31 @@ const ConnectionTestPage: React.FC> = (props) = const [ activeTab, setActiveTab ] = useState(0); const [ showResults, setShowResults ] = useState(false); + const popupInterval = useRef(null); + const fetchTimer = useRef(null); + + /** + * Clears all running timers for popup monitoring and result fetching. + */ + const clearTimers = (): void => { + if (popupInterval.current) { + clearInterval(popupInterval.current); + popupInterval.current = null; + } + if (fetchTimer.current) { + clearTimeout(fetchTimer.current); + fetchTimer.current = null; + } + }; + + /** + * Effect to clear timers on component unmount. + */ + useEffect(() => { + return () => clearTimers(); + }, []); + + /** * Fetches the test results from the backend. */ @@ -91,10 +120,11 @@ const ConnectionTestPage: React.FC> = (props) = try { const axios = (await import("axios")).default; const response = await axios.get( - `https://localhost:9443/api/server/v1/debug/result/${sid}`, + `${resourceEndpoints.debugResult}/${sid}`, { withCredentials: true } ); setResult(response.data); + setShowResults(true); // eslint-disable-next-line no-console console.log("[ConnectionTest] Successfully fetched results:", response.data); @@ -133,28 +163,32 @@ const ConnectionTestPage: React.FC> = (props) = }; const response = await axios.post( - `https://localhost:9443/api/server/v1/debug/`, + resourceEndpoints.debug, payload, { withCredentials: true } ); + const newDebugId = response.data.result.debugId; const authorizationUrl = response.data.result.authorizationUrl; const status = response.data.result.status; setDebugId(newDebugId); + // Clear any existing timers from previous runs + clearTimers(); + // Open authorizationUrl in a new tab if present if (authorizationUrl) { const authPopup = window.open(authorizationUrl, "_blank"); // Monitor the popup and fetch results when it closes - const checkPopupClosed = setInterval(() => { + popupInterval.current = setInterval(() => { try { if (authPopup && authPopup.closed) { - clearInterval(checkPopupClosed); + clearTimers(); // Fetch results after popup closes - setTimeout(() => { + fetchTimer.current = setTimeout(() => { fetchResult(newDebugId); }, 1000); } @@ -164,17 +198,18 @@ const ConnectionTestPage: React.FC> = (props) = }, 500); // Set a timeout to fetch results after 30 seconds in case popup detection doesn't work - setTimeout(() => { - clearInterval(checkPopupClosed); + fetchTimer.current = setTimeout(() => { + clearTimers(); fetchResult(newDebugId); }, 30000); } else { // If no authorizationUrl, fetch results immediately - setTimeout(() => { + fetchTimer.current = setTimeout(() => { fetchResult(newDebugId); }, 2000); } + // Update status based on response if (status === "SUCCESS") { setConnectionStatus("success"); @@ -182,6 +217,7 @@ const ConnectionTestPage: React.FC> = (props) = setConnectionStatus("error"); } } catch (error: any) { + clearTimers(); setConnectionStatus("error"); setAuthStatus("error"); setClaimsStatus("error"); diff --git a/features/admin.core.v1/store/reducers/config.ts b/features/admin.core.v1/store/reducers/config.ts index af2a6bfc271..185a485466a 100644 --- a/features/admin.core.v1/store/reducers/config.ts +++ b/features/admin.core.v1/store/reducers/config.ts @@ -35,53 +35,14 @@ export const commonConfigReducerInitialState: CommonConfigReducerStateInterface< I18nModuleOptionsInterface, UIConfigInterface> = { - deployment: { - __experimental__platformIdP: null, - accountApp: { - basePath: "", - centralAppPath: "", - displayName: "", - path: "", - tenantQualifiedPath: "" - }, - adminApp: { - basePath: "", - displayName: "", - path: "", - tenantQualifiedPath: "" - }, - allowMultipleAppProtocols: undefined, - appBaseName: "", - appBaseNameWithoutTenant: "", - appHomePath: "", - appLoginPath: "", - appLogoutPath: "", - centralDeploymentEnabled: undefined, - clientHost: "", - clientID: "", - clientOrigin: "", - clientOriginWithTenant: "", - customServerHost: "", - developerApp: { - basePath: "", - displayName: "", - path: "", - tenantQualifiedPath: "" - }, - docSiteURL: "", - extensions: null, - helpCenterURL: "", - idpConfigs: null, - loginCallbackUrl: "", - organizationPrefix: "", - regionSelectionEnabled: undefined, - serverHost: "", - serverOrigin: "", - superTenant: "", - tenant: "", - tenantContext: null, - tenantPath: "", - tenantPrefix: "" + deployment: { + __experimental__platformIdP: null, + accountApp: { + basePath: "", + centralAppPath: "", + displayName: "", + path: "", + tenantQualifiedPath: "" }, endpoints: { CORSOrigins: "", @@ -191,11 +152,199 @@ export const commonConfigReducerInitialState: CommonConfigReducerStateInterface< workflowAssociations: "", workflowInstances: "", workflows: "" + adminApp: { + basePath: "", + displayName: "", + path: "", + tenantQualifiedPath: "" + }, + allowMultipleAppProtocols: undefined, + appBaseName: "", + appBaseNameWithoutTenant: "", + appHomePath: "", + appLoginPath: "", + appLogoutPath: "", + centralDeploymentEnabled: undefined, + clientHost: "", + clientID: "", + clientOrigin: "", + clientOriginWithTenant: "", + customServerHost: "", + developerApp: { + basePath: "", + displayName: "", + path: "", + tenantQualifiedPath: "" + }, + docSiteURL: "", + extensions: null, + helpCenterURL: "", + idpConfigs: null, + loginCallbackUrl: "", + organizationPrefix: "", + regionSelectionEnabled: undefined, + serverHost: "", + serverOrigin: "", + superTenant: "", + tenant: "", + tenantContext: null, + tenantPath: "", + tenantPrefix: "" + }, + endpoints: { + CORSOrigins: "", + accountDisabling: "", + accountLocking: "", + accountRecovery: "", + actions: "", + adminAdvisoryBanner: "", + apiRoot: "", + applicationTemplate: "", + applicationTemplateMetadata: "", + applications: "", + asyncStatus: "", + authenticatorTags: "", + authenticators: "", + brandingPreference: "", + brandingPreferenceSubOrg: "", + brandingTextPreference: "", + brandingTextPreferenceSubOrg: "", + breadcrumb: "", + bulk: "", + captchaForSSOLogin: "", + certificates: "", + claims: "", + clientCertificates: "", + createSecret: "", + createSecretType: "", + customAuthenticators: "", + dcrConfiguration: "", + debug: "", + debugResult: "", + deleteSecret: "", + deleteSecretType: "", + deploymentUnits: "", + entitlementPoliciesApi: "", + entitlementPolicyCombiningAlgorithmApi: "", + entitlementPolicyPublishApi: "", + extensionTemplates: "", + extensions: "", + externalClaims: "", + fidoConfigs: "", + flow: "", + flowMeta: "", + getSecret: "", + getSecretList: "", + getSecretType: "", + governanceConnectorCategories: "", + groupMetadata: "", + groups: "", + guests: "", + guestsList: "", + identityProviders: "", + impersonationConfigurations: "", + localAuthenticators: "", + localClaims: "", + loginPolicies: "", + me: "", + multiFactorAuthenticators: "", + myAccountConfigMgt: "", + oidcScopes: "", + organizations: "", + passiveStsConfigurations: "", + passwordExpiry: "", + passwordHistory: "", + passwordPolicies: "", + passwordPoliciesUpdate: "", + passwordPolicy: "", + permission: "", + publicCertificates: "", + remoteLogPublishEndpoint: "", + remoteLogging: "", + requestPathAuthenticators: "", + resendCode: "", + resourceTypes: "", + roles: "", + rolesV2: "", + rolesV3: "", + rolesWithoutOrgPath: "", + rootOrganization: "", + rootUsersOrganization: "", + rulesMetaData: "", + saml2Configurations: "", + saml2Meta: "", + schemas: "", + selfSignUp: "", + serverConfigurations: "", + serverSupportedSchemas: "", + smsTemplates: "", + tenantAssociationApi: "", + tenantManagementApi: "", + tenantSubscriptionApi: "", + tenants: "", + updateSecret: "", + updateSecretType: "", + userSessions: "", + userStores: "", + users: "", + usersOrganization: "", + usersSuperOrganization: "", + validationServiceMgt: "", + validationServiceMgtSubOrg: "", + wellKnown: "", + workflowAssociations: "", + workflowInstances: "", + workflows: "" + }, + features: { + applications: null, + approvals: null, + attributeDialects: null, + certificates: null, + emailTemplates: null, + governanceConnectors: null, + groups: null, + guestUser: null, + identityProviders: null, + oidcScopes: null, + remoteFetchConfig: null, + roles: null, + secretsManagement: null, + userStores: null, + users: null + }, + i18n: null, + ui: { + actions: null, + adminNotice: { + enabled: undefined, + plannedRollOutDate: undefined + }, + announcements: [], + appCopyright: "", + appLogo: { + defaultLogoPath: "", + defaultWhiteLogoPath: "" }, + appName: "", + appTitle: "", + applicationTemplateLoadingStrategy: undefined, + asyncOperationStatusPollingInterval: null, + connectionResourcesUrl: "", + cookiePolicyUrl: "", + customContent: {}, + emailTemplates: { + defaultLogoUrl: "", + defaultWhiteLogoUrl: "" + }, + enableCustomEmailTemplates: undefined, + enableLegacySessionBoundTokenBehaviour: false, + enableOldUIForEmailProvider: undefined, features: { applications: null, approvals: null, attributeDialects: null, + attributeVerification: null, certificates: null, emailTemplates: null, governanceConnectors: null, @@ -209,18 +358,25 @@ export const commonConfigReducerInitialState: CommonConfigReducerStateInterface< userStores: null, users: null }, - i18n: null, - ui: { - actions: null, - adminNotice: { - enabled: undefined, - plannedRollOutDate: undefined + flowExecution: { + enableLegacyFlows: true + }, + googleOneTapEnabledTenants: [], + gravatarConfig: { + defaultImage: "", + fallback: null, + size: null + }, + hiddenAuthenticators: [], + hiddenConnectionTemplates: [], + hiddenUserStores: [], + i18nConfigs: null, + identityProviderTemplates: { + apple: { + enabled: false }, - announcements: [], - appCopyright: "", - appLogo: { - defaultLogoPath: "", - defaultWhiteLogoPath: "" + enterpriseOIDC: { + enabled: false }, appName: "", appTitle: "", @@ -255,15 +411,17 @@ export const commonConfigReducerInitialState: CommonConfigReducerStateInterface< secretsManagement: null, userStores: null, users: null + enterpriseSAML: { + enabled: false + }, + facebook: { + enabled: false }, - flowExecution: { - enableLegacyFlows: true + github: { + enabled: false }, - googleOneTapEnabledTenants: [], - gravatarConfig: { - defaultImage: "", - fallback: null, - size: null + google: { + enabled: false }, hiddenAuthenticators: [], hiddenConnectionTemplates: [], @@ -340,6 +498,59 @@ export const commonConfigReducerInitialState: CommonConfigReducerStateInterface< enabled: false, title: "", url: "" + microsoft: { + enabled: false } + }, + isClaimUniquenessValidationEnabled: undefined, + isClientSecretHashEnabled: undefined, + isCookieConsentBannerEnabled: undefined, + isCustomClaimMappingEnabled: undefined, + isCustomClaimMappingMergeEnabled: undefined, + isDefaultDialectEditingEnabled: undefined, + isDialectAddingEnabled: undefined, + isEditingSystemRolesAllowed: undefined, + isGroupAndRoleSeparationEnabled: undefined, + isHeaderAvatarLabelAllowed: undefined, + isLeftNavigationCategorized: undefined, + isMarketingConsentBannerEnabled: undefined, + isPasswordInputValidationEnabled: undefined, + isRequestPathAuthenticationEnabled: undefined, + isSAASDeployment: undefined, + isSignatureValidationCertificateAliasEnabled: undefined, + isTrustedAppConsentRequired: undefined, + isWSFedProtocolTemplateEnabled: undefined, + listAllAttributeDialects: undefined, + multiTenancy: { + isTenantDomainDotExtensionMandatory: true, + tenantDomainIllegalCharactersRegex: "", + tenantDomainRegex: "" + }, + passwordPolicyConfigs: null, + primaryUserStoreDomainName: "", + privacyPolicyConfigs: null, + productName: "", + productVersionConfig: null, + routes: { + organizationEnabledRoutes: undefined + }, + selfAppIdentifier: "", + showAppSwitchButton: undefined, + showSmsOtpPwdRecoveryFeatureStatusChip: undefined, + showStatusLabelForNewAuthzRuntimeFeatures: undefined, + systemAppsIdentifiers: [], + systemReservedUserStores: [], + theme: { + name: "", + path: "", + styleSheets: null + }, + userSurveyBanner: { + buttonText: "", + description: "", + enabled: false, + title: "", + url: "" } - }; + } +}; From 97efdf8ce0ded7055cdd627a993b3b3bef19b28f Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Tue, 10 Feb 2026 15:45:08 +0530 Subject: [PATCH 11/26] Refactor connection test functionality and improve UI components --- apps/console/src/configs/routes.tsx | 7 +- .../pages/connection-edit.tsx | 86 +++- .../pages/connection-test.tsx | 429 ++++++------------ .../admin.core.v1/constants/app-constants.ts | 1 + 4 files changed, 221 insertions(+), 302 deletions(-) diff --git a/apps/console/src/configs/routes.tsx b/apps/console/src/configs/routes.tsx index 071914629fc..b8e6f2f9d3b 100644 --- a/apps/console/src/configs/routes.tsx +++ b/apps/console/src/configs/routes.tsx @@ -593,10 +593,9 @@ export const getAppViewRoutes = (): RouteInterface[] => { icon: { icon: getSidePanelIcons().childIcon }, - id: "identityProvidersTest", - name: "Identity Providers Test", - // Direct path, since AppConstants does not have a test path - path: "/t/:tenantDomain/console/connections/:idpId/test", + id: "identityProviderTest", + name: "Identity Provider Test", + path: AppConstants.getPaths().get("IDP_TEST"), protected: true, showOnSidePanel: false }, diff --git a/features/admin.connections.v1/pages/connection-edit.tsx b/features/admin.connections.v1/pages/connection-edit.tsx index 2a1ec5bbd2e..6747173fea6 100644 --- a/features/admin.connections.v1/pages/connection-edit.tsx +++ b/features/admin.connections.v1/pages/connection-edit.tsx @@ -21,6 +21,7 @@ import { ApplicationTemplateConstants } from "@wso2is/admin.application-template import { AppConstants } from "@wso2is/admin.core.v1/constants/app-constants"; import { history } from "@wso2is/admin.core.v1/helpers/history"; import useUIConfig from "@wso2is/admin.core.v1/hooks/use-ui-configs"; +import useResourceEndpoints from "@wso2is/admin.core.v1/hooks/use-resource-endpoints"; import { FeatureConfigInterface } from "@wso2is/admin.core.v1/models/config"; import { AppState } from "@wso2is/admin.core.v1/store"; import { @@ -68,7 +69,7 @@ import { } from "../models/connection"; import { Icon } from "semantic-ui-react"; import { - PrimaryButton, + PrimaryButton, SecondaryButton } from "@wso2is/react-components"; import { ConnectionTemplateManagementUtils } from "../utils/connection-template-utils"; import { ConnectionsManagementUtils, handleGetConnectionsMetaDataError } from "../utils/connection-utils"; @@ -94,6 +95,7 @@ const ConnectionEditPage: FunctionComponent = const { t } = useTranslation(); const { UIConfig } = useUIConfig(); + const { resourceEndpoints } = useResourceEndpoints(); const setConnectionTemplates: (templates: Record[]) => void = useSetConnectionTemplates(); const idpDescElement: React.MutableRefObject = useRef(null); @@ -121,6 +123,7 @@ const ConnectionEditPage: FunctionComponent = const [ isConnectorMetaDataFetchRequestLoading, setConnectorMetaDataFetchRequestLoading ] = useState( undefined ); + const [ isTestingConnection, setIsTestingConnection ] = useState(false); const isReadOnly: boolean = !useRequiredScopes(featureConfig?.identityProviders?.scopes?.update); const hasApplicationTemplateViewPermissions: boolean = useRequiredScopes(applicationsFeatureConfig?.scopes?.read); @@ -686,6 +689,71 @@ const ConnectionEditPage: FunctionComponent = ); }; + /** + * Handles the test connection button click event. + * Starts a debug session and redirects to the test page with the debug data. + */ + const handleTestConnection = async (): Promise => { + if (!connector?.id) { + return; + } + + setIsTestingConnection(true); + + try { + const axios = (await import("axios")).default; + const payload = { + resourceId: connector.id, + resourceType: "IDP", + properties: {} + }; + + const response = await axios.post( + resourceEndpoints.debug, + payload, + { withCredentials: true } + ); + + const debugId = response.data?.result?.debugId; + const authorizationUrl = response.data?.result?.authorizationUrl; + + if (debugId && authorizationUrl) { + // Navigate to test page with debug session data + const pathParts = location.pathname.split("/"); + const tenantIndex = pathParts.indexOf("t"); + const tenantDomain = tenantIndex !== -1 ? pathParts[tenantIndex + 1] : "carbon.super"; + + history.push({ + pathname: `/t/${tenantDomain}/console/connections/${connector.id}/test`, + state: { + debugId, + authorizationUrl + } + }); + } else { + // Handle case where debug session data is incomplete + dispatch( + addAlert({ + description: t("authenticationProvider:notifications.testConnection.error.description"), + level: AlertLevels.ERROR, + message: t("authenticationProvider:notifications.testConnection.error.message") + }) + ); + } + } catch (error: any) { + dispatch( + addAlert({ + description: error?.response?.data?.message || error?.message || + t("authenticationProvider:notifications.testConnection.genericError.description"), + level: AlertLevels.ERROR, + message: t("authenticationProvider:notifications.testConnection.genericError.message") + }) + ); + } finally { + setIsTestingConnection(false); + } + }; + return ( = } } title={ resolveConnectorName(connector) } action={ ConnectionsManagementUtils.isConnectorIdentityProvider(connector) ? ( - { - // Get tenant domain from location.pathname - const pathParts = location.pathname.split("/"); - const tenantIndex = pathParts.indexOf("t"); - const tenantDomain = tenantIndex !== -1 ? pathParts[tenantIndex + 1] : "carbon.super"; - const idpId = connector?.id; - if (!idpId) return; - history.push(`/t/${tenantDomain}/console/connections/${idpId}/test`); - }} + onClick={ handleTestConnection } + loading={ isTestingConnection } + disabled={ isTestingConnection } > Test Connection - + ) : null } contentTopMargin={ true } description={ resolveConnectorDescription(connector) } diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index eda003f4cd9..393e5dbd2fc 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -22,24 +22,15 @@ import { history } from "@wso2is/admin.core.v1/helpers/history"; import { - PageLayout, PrimaryButton, TabPageLayout, - AppAvatar, - AnimatedAvatar + ContentLoader } from "@wso2is/react-components"; -import React, { useState, useRef, Fragment, useEffect } from "react"; -import useUIConfig from "@wso2is/admin.core.v1/hooks/use-ui-configs"; +import React, { useState, useRef, useEffect } from "react"; import useResourceEndpoints from "@wso2is/admin.core.v1/hooks/use-resource-endpoints"; - import { useTranslation } from "react-i18next"; import { RouteComponentProps } from "react-router-dom"; -import { Header, Icon, List, Segment, Tab } from "semantic-ui-react"; -import { ConnectionsManagementUtils } from "@wso2is/admin.connections.v1/utils/connection-utils"; -import { AuthenticatorMeta } from "../meta/authenticator-meta"; -import { getConnectionDetails } from "../api/connections"; -import { EmptyPlaceholder } from "@wso2is/react-components"; -import { getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1/configs/ui"; +import { Header, Icon, Segment, Tab, Placeholder } from "semantic-ui-react"; /** @@ -47,14 +38,9 @@ import { getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1/configs/ */ interface RouteParams { tenantDomain?: string; - idpId?: string; + id?: string; } -/** - * Enum for test status. - */ -type TestStatus = "idle" | "pending" | "success" | "error"; - /** * Connection Test page component. * @@ -62,22 +48,19 @@ type TestStatus = "idle" | "pending" | "success" | "error"; * @returns React element. */ const ConnectionTestPage: React.FC> = (props) => { - const { tenantDomain = "", idpId = "" } = props.match.params || {}; + const { id: idpId = "" } = props.match.params || {}; + const { location } = props; const { t } = useTranslation(); const { resourceEndpoints } = useResourceEndpoints(); - - const [ connectionStatus, setConnectionStatus ] = useState("idle"); - const [ authStatus, setAuthStatus ] = useState("idle"); - const [ claimsStatus, setClaimsStatus ] = useState("idle"); const [ debugId, setDebugId ] = useState(null); const [ result, setResult ] = useState(null); const [ error, setError ] = useState(null); const [ loading, setLoading ] = useState(false); const [ hasError, setHasError ] = useState(false); const [ activeTab, setActiveTab ] = useState(0); - const [ showResults, setShowResults ] = useState(false); + const [ autoRunTriggered, setAutoRunTriggered ] = useState(false); const popupInterval = useRef(null); const fetchTimer = useRef(null); @@ -103,6 +86,42 @@ const ConnectionTestPage: React.FC> = (props) = return () => clearTimers(); }, []); + /** + * Auto-run test if debugId and authUrl are provided via location state. + */ + useEffect(() => { + const state = location?.state as any; + + if (state?.debugId && state?.authorizationUrl && !autoRunTriggered) { + setAutoRunTriggered(true); + setDebugId(state.debugId); + + // Open the authorization URL + const authPopup = window.open(state.authorizationUrl, "_blank"); + + // Monitor the popup + popupInterval.current = setInterval(() => { + try { + if (authPopup && authPopup.closed) { + clearTimers(); + // Fetch results after popup closes + fetchTimer.current = setTimeout(() => { + fetchResult(state.debugId); + }, 1000); + } + } catch (e) { + // ignore + } + }, 500); + + // Set a timeout to fetch results after 30 seconds + fetchTimer.current = setTimeout(() => { + clearTimers(); + fetchResult(state.debugId); + }, 30000); + } + }, [location, autoRunTriggered]); + /** * Fetches the test results from the backend. @@ -125,7 +144,6 @@ const ConnectionTestPage: React.FC> = (props) = ); setResult(response.data); - setShowResults(true); // eslint-disable-next-line no-console console.log("[ConnectionTest] Successfully fetched results:", response.data); } catch (err: any) { @@ -147,12 +165,17 @@ const ConnectionTestPage: React.FC> = (props) = * Executes POST request to test connection. */ const handleRunTests = async (): Promise => { - setConnectionStatus("pending"); - setAuthStatus("pending"); - setClaimsStatus("pending"); setError(null); setResult(null); - setShowResults(false); + setHasError(false); + + // eslint-disable-next-line no-console + console.log("[ConnectionTest] Starting rerun test for idpId:", idpId); + + if (!idpId) { + setError("Connection ID is missing. Cannot run test."); + return; + } try { const axios = (await import("axios")).default; @@ -162,16 +185,20 @@ const ConnectionTestPage: React.FC> = (props) = properties: {} }; + // eslint-disable-next-line no-console + console.log("[ConnectionTest] Posting debug request with payload:", payload); + const response = await axios.post( resourceEndpoints.debug, payload, { withCredentials: true } ); - const newDebugId = response.data.result.debugId; const authorizationUrl = response.data.result.authorizationUrl; - const status = response.data.result.status; + + // eslint-disable-next-line no-console + console.log("[ConnectionTest] Debug session created:", { newDebugId, authorizationUrl }); setDebugId(newDebugId); @@ -208,30 +235,18 @@ const ConnectionTestPage: React.FC> = (props) = fetchResult(newDebugId); }, 2000); } - - - // Update status based on response - if (status === "SUCCESS") { - setConnectionStatus("success"); - } else { - setConnectionStatus("error"); - } } catch (error: any) { + // eslint-disable-next-line no-console + console.error("[ConnectionTest] Error running test:", error); clearTimers(); - setConnectionStatus("error"); - setAuthStatus("error"); - setClaimsStatus("error"); setError(error?.response?.data?.message || error?.message || "Failed to run test."); } }; - // Update test statuses based on result metadata + // Update error status based on result metadata useEffect(() => { if (result && result.metadata) { const metadata = result.metadata; - setConnectionStatus(metadata.connectionStatus === "success" ? "success" : metadata.connectionStatus === "pending" ? "pending" : "error"); - setAuthStatus(metadata.authenticationStatus === "success" ? "success" : metadata.authenticationStatus === "pending" ? "pending" : "error"); - setClaimsStatus(metadata.claimMappingStatus === "success" ? "success" : metadata.claimMappingStatus === "pending" ? "pending" : "error"); // Check if any step explicitly failed (not pending or success) const hasStepError = ( @@ -254,143 +269,25 @@ const ConnectionTestPage: React.FC> = (props) = * Handles the back button click event. */ const handleBackButtonClick = (): void => { + // Extract tenant domain from current path + const pathParts = location.pathname.split("/"); + const tenantIndex = pathParts.indexOf("t"); + const tenantDomain = tenantIndex !== -1 ? pathParts[tenantIndex + 1] : "carbon.super"; + // Navigate to the specific connection page history.push(`/t/${tenantDomain}/console/connections/${idpId}`); }; - /** - * Handles the "Back to Test" button when viewing results. - */ - const handleBackToTest = (): void => { - setShowResults(false); - setResult(null); - setError(null); - setDebugId(null); - setConnectionStatus("idle"); - setAuthStatus("idle"); - setClaimsStatus("idle"); - setHasError(false); - setActiveTab(0); - }; - - /** - * Renders the status icon based on the test status. - * - * @param status - The status of the test. - * @returns React element. - */ - const renderStatusIcon = (status: TestStatus) => { - switch (status) { - case "pending": - // Use spinner for "pending" (running) state - return ; - case "success": - return ; - case "error": - return ; - case "idle": - default: - // Use clock icon for "idle" state (matching wireframe's "Pending") - return ; - } - }; - - - // State for connector details - const [ connector, setConnector ] = useState(undefined); - const [ isConnectorDetailsFetchRequestLoading, setConnectorDetailFetchRequestLoading ] = useState(false); - // Get UIConfig and connectionResourcesUrl like in edit page - const { UIConfig } = useUIConfig(); - const connectionResourcesUrl = UIConfig?.connectionResourcesUrl; - const idpDescElement = useRef(null); + // State for connector details - removed as we no longer display connector info on this page const testId = "idp-test-connection"; - // Fetch connector details on mount - React.useEffect(() => { - if (!idpId) return; - setConnectorDetailFetchRequestLoading(true); - (async () => { - try { - const response = await getConnectionDetails(idpId); - setConnector(response); - } catch (error) { - setConnector(undefined); - } finally { - setConnectorDetailFetchRequestLoading(false); - } - })(); - }, [idpId]); - - const resolveConnectorName = (conn) => { - if (!conn) { - return "Connection Test"; - } - - if (ConnectionsManagementUtils.isConnectorIdentityProvider(conn)) { - return conn.name; - } - - const name = conn.friendlyName || conn.displayName || conn.name || "Connection Test"; - - return name; - }; - - const resolveConnectorImage = (conn) => { - const isOrganizationSSOIDP: boolean = ConnectionsManagementUtils.isOrganizationSSOConnection( - conn?.federatedAuthenticators?.defaultAuthenticatorId - ); - - if (!conn) { - return ; - } - - if (ConnectionsManagementUtils.isConnectorIdentityProvider(conn) && !isOrganizationSSOIDP) { - if (conn.image) { - const resolvedPath = ConnectionsManagementUtils.resolveConnectionResourcePath( - connectionResourcesUrl, - conn?.image - ); - return ( - - ); - } - return ; - } - return ( - - ); - }; - - const resolveConnectorDescription = (conn) => { - if (!conn) { - return "Test Federated IdP connection"; - } - return `Test the ${conn?.name || "connection"} connection`; - }; - /** * Renders the results tabs when test results are available. */ const renderResultsTabs = () => { const tabPanes = [ { - menuItem: "Token Details", + menuItem: "ID Token", render: () => { // Helper to decode JWT const decodeJWT = (token) => { @@ -417,10 +314,9 @@ const ConnectionTestPage: React.FC> = (props) = return ( -
+
{decoded ? ( -
-
ID Token
+
> = (props) =
) : ( -
+
Unable to decode token or not a valid JWT.
)} -
External Redirect URL
+
External Redirect URL
                                     {typeof result?.externalRedirectUrl === 'string' ? result.externalRedirectUrl : JSON.stringify(result?.externalRedirectUrl, null, 2)}
                                 
@@ -500,8 +396,7 @@ const ConnectionTestPage: React.FC> = (props) = return (
-
Claims Mapping Table
-
+
@@ -569,7 +464,6 @@ const ConnectionTestPage: React.FC> = (props) = {Object.keys(metadata).length > 0 && (
-
Steps
{[ { key: 'connectionStatus', label: 'Connection Creation' }, @@ -622,115 +516,68 @@ const ConnectionTestPage: React.FC> = (props) = return ( - - Rerun Test - - ) : null + + + Rerun Test + } titleTextAlign="left" contentTopMargin={true} bottomMargin={false} data-testid={`${testId}-page-layout`} > - { !showResults && ( - <> -
- - - Run Tests - + {/* Test Status Banner */} + {result && !error && ( + +
+ +
+
+ {hasError ? 'Test Failed' : 'Test Passed'} +
+

+ {hasError + ? 'Some steps failed during the connection test. Check the Logs tab for details.' + : 'All connection test steps completed successfully.'} +

+
- - - ) } - - - {showResults && ( - <> - {/* Test Status Section */} - -
- {t("console:develop.pages.idpTest.statusHeader", "Test Status")} -
- {hasError && ( -
-

⚠️ Errors Detected

-

Check the details below about the failed steps.

-
- )} - - - - {renderStatusIcon(connectionStatus)} - - - - {t("console:develop.pages.idpTest.connectionCreation", "Connection Creation")} - - - - - - {renderStatusIcon(authStatus)} - - - - {t("console:develop.pages.idpTest.authentication", "Authentication")} - - - - - - {renderStatusIcon(claimsStatus)} - - - - {t("console:develop.pages.idpTest.claimsMapping", "Claims Mapping")} - - - - -
- - {/* Results Section */} +
+ )} + + {/* Results Section */} {error && ( > = (props) = )} - {!error && !result && debugId && !loading && ( + {!error && !result && (debugId || loading) && ( - No results available. -
- fetchResult(debugId)} data-testid="idp-test-result-refresh"> - Refresh - +
+ + + {loading ? 'Loading test results...' : 'Waiting for authentication to complete...'} +
+ + {/* Skeleton Loader */} + + + + + + + + + + )} - - )} ); }; diff --git a/features/admin.core.v1/constants/app-constants.ts b/features/admin.core.v1/constants/app-constants.ts index a418757d4ac..34d10a3019f 100644 --- a/features/admin.core.v1/constants/app-constants.ts +++ b/features/admin.core.v1/constants/app-constants.ts @@ -342,6 +342,7 @@ export class AppConstants { [ "IDP", `${ AppConstants.getDeveloperViewBasePath() }/connections` ], [ "IDP_TEMPLATES", `${ AppConstants.getDeveloperViewBasePath() }/connections/templates` ], [ "IDP_EDIT", `${ AppConstants.getDeveloperViewBasePath() }/connections/:id` ], + [ "IDP_TEST", `${ AppConstants.getDeveloperViewBasePath() }/connections/:id/test` ], [ "AUTH_EDIT", `${ AppConstants.getDeveloperViewBasePath() }/authenticators/:id` ], [ "IDVP", `${ AppConstants.getDeveloperViewBasePath() }/identity-verification-providers` ], [ "IDVP_TEMPLATES", From 71a0fefce0938daa27c267b000ed3eb9960a7472 Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Tue, 24 Feb 2026 12:29:04 +0530 Subject: [PATCH 12/26] Refactor connection ID handling --- .../pages/connection-edit.tsx | 11 ++-- .../pages/connection-test.tsx | 53 ++++++++++--------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/features/admin.connections.v1/pages/connection-edit.tsx b/features/admin.connections.v1/pages/connection-edit.tsx index 6747173fea6..f02d44ed83d 100644 --- a/features/admin.connections.v1/pages/connection-edit.tsx +++ b/features/admin.connections.v1/pages/connection-edit.tsx @@ -70,7 +70,7 @@ import { import { Icon } from "semantic-ui-react"; import { PrimaryButton, SecondaryButton -} from "@wso2is/react-components"; +} from "@wso2is/oxygen-ui"; import { ConnectionTemplateManagementUtils } from "../utils/connection-template-utils"; import { ConnectionsManagementUtils, handleGetConnectionsMetaDataError } from "../utils/connection-utils"; @@ -703,9 +703,10 @@ const ConnectionEditPage: FunctionComponent = try { const axios = (await import("axios")).default; const payload = { - resourceId: connector.id, resourceType: "IDP", - properties: {} + properties: { + connectionId: connector.id + } }; const response = await axios.post( @@ -714,8 +715,8 @@ const ConnectionEditPage: FunctionComponent = { withCredentials: true } ); - const debugId = response.data?.result?.debugId; - const authorizationUrl = response.data?.result?.authorizationUrl; + const debugId = response.data?.debugId; + const authorizationUrl = response.data?.metadata?.authorizationUrl; if (debugId && authorizationUrl) { // Navigate to test page with debug session data diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index 393e5dbd2fc..ee7a31add86 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -48,7 +48,7 @@ interface RouteParams { * @returns React element. */ const ConnectionTestPage: React.FC> = (props) => { - const { id: idpId = "" } = props.match.params || {}; + const { id: connectorId = "" } = props.match.params || {}; const { location } = props; const { t } = useTranslation(); @@ -170,9 +170,9 @@ const ConnectionTestPage: React.FC> = (props) = setHasError(false); // eslint-disable-next-line no-console - console.log("[ConnectionTest] Starting rerun test for idpId:", idpId); + console.log("[ConnectionTest] Starting rerun test for connectorId:", connectorId); - if (!idpId) { + if (!connectorId) { setError("Connection ID is missing. Cannot run test."); return; } @@ -180,9 +180,10 @@ const ConnectionTestPage: React.FC> = (props) = try { const axios = (await import("axios")).default; const payload = { - resourceId: idpId, resourceType: "IDP", - properties: {} + properties: { + connectionId: connectorId + } }; // eslint-disable-next-line no-console @@ -194,8 +195,8 @@ const ConnectionTestPage: React.FC> = (props) = { withCredentials: true } ); - const newDebugId = response.data.result.debugId; - const authorizationUrl = response.data.result.authorizationUrl; + const newDebugId = response.data.debugId; + const authorizationUrl = response.data.metadata.authorizationUrl; // eslint-disable-next-line no-console console.log("[ConnectionTest] Debug session created:", { newDebugId, authorizationUrl }); @@ -245,14 +246,14 @@ const ConnectionTestPage: React.FC> = (props) = // Update error status based on result metadata useEffect(() => { - if (result && result.metadata) { - const metadata = result.metadata; - + if (result && result.metadata && result.metadata.steps) { + const steps = result.metadata.steps; + // Check if any step explicitly failed (not pending or success) const hasStepError = ( - (metadata.connectionStatus !== "success" && metadata.connectionStatus !== "pending") || - (metadata.authenticationStatus !== "success" && metadata.authenticationStatus !== "pending") || - (metadata.claimMappingStatus !== "success" && metadata.claimMappingStatus !== "pending") || + (steps.connectionStatus !== "success" && steps.connectionStatus !== "pending") || + (steps.authenticationStatus !== "success" && steps.authenticationStatus !== "pending") || + (steps.claimMappingStatus !== "success" && steps.claimMappingStatus !== "pending") || result?.error ); @@ -275,7 +276,7 @@ const ConnectionTestPage: React.FC> = (props) = const tenantDomain = tenantIndex !== -1 ? pathParts[tenantIndex + 1] : "carbon.super"; // Navigate to the specific connection page - history.push(`/t/${tenantDomain}/console/connections/${idpId}`); + history.push(`/t/${tenantDomain}/console/connections/${connectorId}`); }; // State for connector details - removed as we no longer display connector info on this page @@ -309,7 +310,7 @@ const ConnectionTestPage: React.FC> = (props) = }; }; - const idToken = result?.idToken; + const idToken = result?.metadata?.idToken; const decoded = decodeJWT(idToken); return ( @@ -370,7 +371,7 @@ const ConnectionTestPage: React.FC> = (props) = )}
External Redirect URL
-                                    {typeof result?.externalRedirectUrl === 'string' ? result.externalRedirectUrl : JSON.stringify(result?.externalRedirectUrl, null, 2)}
+                                    {typeof result?.metadata?.externalRedirectUrl === 'string' ? result.metadata.externalRedirectUrl : JSON.stringify(result?.metadata?.externalRedirectUrl, null, 2)}
                                 
@@ -380,7 +381,7 @@ const ConnectionTestPage: React.FC> = (props) = { menuItem: "Claims Mappings", render: () => { - const claimsArray = Array.isArray(result?.mappedClaims) ? result?.mappedClaims : []; + const claimsArray = Array.isArray(result?.metadata?.mappedClaims) ? result?.metadata?.mappedClaims : []; const sortedClaims = claimsArray.sort((a, b) => { if (a.status === 'Successful' && b.status !== 'Successful') return -1; if (a.status !== 'Successful' && b.status === 'Successful') return 1; @@ -432,9 +433,9 @@ const ConnectionTestPage: React.FC> = (props) = menuItem: "Logs", render: () => { const formatLogs = () => { - const metadata = result?.metadata || {}; - const errorDetails = result?.error_details || null; - const errorDescription = result?.error_description || null; + const steps = result?.metadata?.steps || {}; + const errorDetails = result?.metadata?.error_details || null; + const errorDescription = result?.metadata?.error_description || null; return (
@@ -462,7 +463,7 @@ const ConnectionTestPage: React.FC> = (props) =
)} - {Object.keys(metadata).length > 0 && ( + {Object.keys(steps).length > 0 && (
{[ @@ -470,7 +471,7 @@ const ConnectionTestPage: React.FC> = (props) = { key: 'authenticationStatus', label: 'Authentication' }, { key: 'claimMappingStatus', label: 'Claims Mapping' } ].map(({ key, label }) => ( - key in metadata ? ( + key in steps ? (
{label}: > = (props) = fontSize: 13, padding: '2px 8px', borderRadius: 4, - background: metadata[key] === 'success' ? '#e6ffe6' : metadata[key] === 'failed' || metadata[key] === 'error' ? '#ffe6e6' : '#f0f0f0', - color: metadata[key] === 'success' ? '#21ba45' : metadata[key] === 'failed' || metadata[key] === 'error' ? '#db2828' : '#666', + background: steps[key] === 'success' ? '#e6ffe6' : steps[key] === 'failed' || steps[key] === 'error' ? '#ffe6e6' : '#f0f0f0', + color: steps[key] === 'success' ? '#21ba45' : steps[key] === 'failed' || steps[key] === 'error' ? '#db2828' : '#666', fontWeight: 500 }}> - {String(metadata[key])} + {String(steps[key])}
) : null @@ -491,7 +492,7 @@ const ConnectionTestPage: React.FC> = (props) =
)} - {!errorDetails && !errorDescription && Object.keys(metadata).length === 0 && ( + {!errorDetails && !errorDescription && Object.keys(steps).length === 0 && (
No log information available.
From 2bf2e13dfda244bfc5006048c100f35eb2dea1e1 Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Wed, 11 Mar 2026 13:48:40 +0530 Subject: [PATCH 13/26] Refactor connection debug endpoints --- .../admin.connections.v1/configs/endpoints.ts | 2 +- .../pages/connection-edit.tsx | 31 +++++---- .../pages/connection-test.tsx | 69 ++++++++++++++----- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/features/admin.connections.v1/configs/endpoints.ts b/features/admin.connections.v1/configs/endpoints.ts index a22f2532781..222a3bcca84 100644 --- a/features/admin.connections.v1/configs/endpoints.ts +++ b/features/admin.connections.v1/configs/endpoints.ts @@ -39,6 +39,6 @@ export const getConnectionResourceEndpoints = (serverHost: string): ConnectionRe CommonAuthenticatorConstants.MFA_CONNECTOR_CATEGORY_ID }`, debug: `${ serverHost }/api/server/v1/debug`, - debugResult: `${ serverHost }/api/server/v1/debug/result` + debugResult: `${ serverHost }/api/server/v1/debug` }; }; diff --git a/features/admin.connections.v1/pages/connection-edit.tsx b/features/admin.connections.v1/pages/connection-edit.tsx index f02d44ed83d..10ac29de788 100644 --- a/features/admin.connections.v1/pages/connection-edit.tsx +++ b/features/admin.connections.v1/pages/connection-edit.tsx @@ -70,7 +70,7 @@ import { import { Icon } from "semantic-ui-react"; import { PrimaryButton, SecondaryButton -} from "@wso2is/oxygen-ui"; +} from "@wso2is/react-components"; import { ConnectionTemplateManagementUtils } from "../utils/connection-template-utils"; import { ConnectionsManagementUtils, handleGetConnectionsMetaDataError } from "../utils/connection-utils"; @@ -695,6 +695,14 @@ const ConnectionEditPage: FunctionComponent = */ const handleTestConnection = async (): Promise => { if (!connector?.id) { + dispatch( + addAlert({ + description: "Connection ID is not available.", + level: AlertLevels.ERROR, + message: t("authenticationProvider:notifications.getIDP.error.message") + }) + ); + return; } @@ -702,16 +710,10 @@ const ConnectionEditPage: FunctionComponent = try { const axios = (await import("axios")).default; - const payload = { - resourceType: "IDP", - properties: { - connectionId: connector.id - } - }; const response = await axios.post( - resourceEndpoints.debug, - payload, + `${resourceEndpoints.debug}/idp`, + { connectionId: connector.id }, { withCredentials: true } ); @@ -735,19 +737,20 @@ const ConnectionEditPage: FunctionComponent = // Handle case where debug session data is incomplete dispatch( addAlert({ - description: t("authenticationProvider:notifications.testConnection.error.description"), + description: t("authenticationProvider:notifications.getIDP.error.description", + { description: "Debug session data is incomplete." }), level: AlertLevels.ERROR, - message: t("authenticationProvider:notifications.testConnection.error.message") + message: t("authenticationProvider:notifications.getIDP.error.message") }) ); } } catch (error: any) { dispatch( addAlert({ - description: error?.response?.data?.message || error?.message || - t("authenticationProvider:notifications.testConnection.genericError.description"), + description: error?.response?.data?.message || error?.response?.data?.description || + error?.message || t("authenticationProvider:notifications.getIDP.genericError.description"), level: AlertLevels.ERROR, - message: t("authenticationProvider:notifications.testConnection.genericError.message") + message: t("authenticationProvider:notifications.getIDP.genericError.message") }) ); } finally { diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index ee7a31add86..d2f2d066ad8 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -139,7 +139,7 @@ const ConnectionTestPage: React.FC> = (props) = try { const axios = (await import("axios")).default; const response = await axios.get( - `${resourceEndpoints.debugResult}/${sid}`, + `${resourceEndpoints.debug}/${sid}/result`, { withCredentials: true } ); setResult(response.data); @@ -180,17 +180,14 @@ const ConnectionTestPage: React.FC> = (props) = try { const axios = (await import("axios")).default; const payload = { - resourceType: "IDP", - properties: { - connectionId: connectorId - } + connectionId: connectorId }; // eslint-disable-next-line no-console console.log("[ConnectionTest] Posting debug request with payload:", payload); const response = await axios.post( - resourceEndpoints.debug, + `${resourceEndpoints.debug}/idp`, payload, { withCredentials: true } ); @@ -246,18 +243,31 @@ const ConnectionTestPage: React.FC> = (props) = // Update error status based on result metadata useEffect(() => { - if (result && result.metadata && result.metadata.steps) { - const steps = result.metadata.steps; + if (result) { + // Handle nested metadata structure + const metadataObj = result?.metadata?.metadata || result?.metadata || {}; + const steps = metadataObj?.steps || { + connectionStatus: metadataObj?.connectionStatus, + authenticationStatus: metadataObj?.authenticationStatus, + claimMappingStatus: metadataObj?.claimMappingStatus, + claimExtractionStatus: metadataObj?.claimExtractionStatus + }; + // Check for top-level FAILURE status first + const topLevelFailure = result?.status === "FAILURE"; + // Check if any step explicitly failed (not pending or success) const hasStepError = ( - (steps.connectionStatus !== "success" && steps.connectionStatus !== "pending") || - (steps.authenticationStatus !== "success" && steps.authenticationStatus !== "pending") || - (steps.claimMappingStatus !== "success" && steps.claimMappingStatus !== "pending") || + (steps.connectionStatus && steps.connectionStatus !== "success" && steps.connectionStatus !== "pending") || + (steps.authenticationStatus && steps.authenticationStatus !== "success" && steps.authenticationStatus !== "pending") || + (steps.claimMappingStatus && steps.claimMappingStatus !== "success" && steps.claimMappingStatus !== "pending") || result?.error ); - if (hasStepError) { + // Check for error fields in metadata + const hasErrorFields = metadataObj?.error_details || metadataObj?.error_description || metadataObj?.error_code; + + if (topLevelFailure || hasStepError || hasErrorFields) { setHasError(true); setActiveTab(2); // Switch to Logs tab (index 2) } else { @@ -433,16 +443,41 @@ const ConnectionTestPage: React.FC> = (props) = menuItem: "Logs", render: () => { const formatLogs = () => { - const steps = result?.metadata?.steps || {}; - const errorDetails = result?.metadata?.error_details || null; - const errorDescription = result?.metadata?.error_description || null; + // Handle nested metadata structure + const metadataObj = result?.metadata?.metadata || result?.metadata || {}; + const steps = metadataObj?.steps || { + connectionStatus: metadataObj?.connectionStatus, + authenticationStatus: metadataObj?.authenticationStatus, + claimMappingStatus: metadataObj?.claimMappingStatus, + claimExtractionStatus: metadataObj?.claimExtractionStatus + }; + const errorDetails = metadataObj?.error_details || result?.metadata?.error_details || null; + const errorDescription = metadataObj?.error_description || result?.metadata?.error_description || null; + const errorCode = metadataObj?.error_code || result?.metadata?.error_code || null; + const topLevelStatus = result?.status; return (
- {(errorDetails || errorDescription) && ( + {topLevelStatus === "FAILURE" && ( +
+
+ Test Status: FAILURE +
+
+ )} + + {(errorDetails || errorDescription || errorCode) && (
Error Information
+ {errorCode && ( +
+ Error Code: +

+ {errorCode} +

+
+ )} {errorDescription && (
Description: @@ -492,7 +527,7 @@ const ConnectionTestPage: React.FC> = (props) =
)} - {!errorDetails && !errorDescription && Object.keys(steps).length === 0 && ( + {!errorDetails && !errorDescription && !errorCode && Object.keys(steps).length === 0 && topLevelStatus !== "FAILURE" && (
No log information available.
From d4c9b1df4a6f3464cd77c6fea1525a3d6bc2f55d Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Wed, 25 Mar 2026 12:04:29 +0530 Subject: [PATCH 14/26] Refactor connection edit and test page components --- .../pages/connection-edit.tsx | 4 +- .../pages/connection-test.tsx | 54 ++++++++----------- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/features/admin.connections.v1/pages/connection-edit.tsx b/features/admin.connections.v1/pages/connection-edit.tsx index 10ac29de788..4e62abba7c0 100644 --- a/features/admin.connections.v1/pages/connection-edit.tsx +++ b/features/admin.connections.v1/pages/connection-edit.tsx @@ -775,13 +775,11 @@ const ConnectionEditPage: FunctionComponent = action={ ConnectionsManagementUtils.isConnectorIdentityProvider(connector) ? ( - + Test Connection ) : null } diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index d2f2d066ad8..cecc7e30ebd 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -389,7 +389,7 @@ const ConnectionTestPage: React.FC> = (props) = } }, { - menuItem: "Claims Mappings", + menuItem: "Claim Mappings", render: () => { const claimsArray = Array.isArray(result?.metadata?.mappedClaims) ? result?.metadata?.mappedClaims : []; const sortedClaims = claimsArray.sort((a, b) => { @@ -440,33 +440,27 @@ const ConnectionTestPage: React.FC> = (props) = } }, { - menuItem: "Logs", + menuItem: "Diagnostics", render: () => { const formatLogs = () => { // Handle nested metadata structure - const metadataObj = result?.metadata?.metadata || result?.metadata || {}; - const steps = metadataObj?.steps || { - connectionStatus: metadataObj?.connectionStatus, - authenticationStatus: metadataObj?.authenticationStatus, - claimMappingStatus: metadataObj?.claimMappingStatus, - claimExtractionStatus: metadataObj?.claimExtractionStatus + const metadataObj = result?.metadata || {}; + + // Get steps from stepStatus or individual status fields + const stepStatus = metadataObj?.stepStatus || {}; + const steps = { + connectionStatus: stepStatus?.connectionStatus || metadataObj?.connectionStatus, + authenticationStatus: stepStatus?.authenticationStatus || metadataObj?.authenticationStatus, + claimMappingStatus: stepStatus?.claimMappingStatus || metadataObj?.claimMappingStatus, + claimExtractionStatus: stepStatus?.claimExtractionStatus || metadataObj?.claimExtractionStatus }; - const errorDetails = metadataObj?.error_details || result?.metadata?.error_details || null; - const errorDescription = metadataObj?.error_description || result?.metadata?.error_description || null; - const errorCode = metadataObj?.error_code || result?.metadata?.error_code || null; + const errorDescription = metadataObj?.error_description || null; + const errorCode = metadataObj?.error_code || null; const topLevelStatus = result?.status; return (
- {topLevelStatus === "FAILURE" && ( -
-
- Test Status: FAILURE -
-
- )} - - {(errorDetails || errorDescription || errorCode) && ( + {(errorDescription || errorCode) && (
Error Information
@@ -479,34 +473,27 @@ const ConnectionTestPage: React.FC> = (props) =
)} {errorDescription && ( -
+
Description:

{errorDescription}

)} - {errorDetails && ( -
- Details: -

- {errorDetails} -

-
- )}
)} {Object.keys(steps).length > 0 && (
+
Step Status
{[ { key: 'connectionStatus', label: 'Connection Creation' }, { key: 'authenticationStatus', label: 'Authentication' }, - { key: 'claimMappingStatus', label: 'Claims Mapping' } + { key: 'claimMappingStatus', label: 'Claims Mapping' }, ].map(({ key, label }) => ( - key in steps ? ( + steps[key] ? (
{label}: > = (props) = borderRadius: 4, background: steps[key] === 'success' ? '#e6ffe6' : steps[key] === 'failed' || steps[key] === 'error' ? '#ffe6e6' : '#f0f0f0', color: steps[key] === 'success' ? '#21ba45' : steps[key] === 'failed' || steps[key] === 'error' ? '#db2828' : '#666', - fontWeight: 500 + fontWeight: 500, + textTransform: 'capitalize' }}> {String(steps[key])} @@ -527,7 +515,7 @@ const ConnectionTestPage: React.FC> = (props) =
)} - {!errorDetails && !errorDescription && !errorCode && Object.keys(steps).length === 0 && topLevelStatus !== "FAILURE" && ( + {!errorDescription && !errorCode && Object.keys(steps).length === 0 && topLevelStatus !== "FAILURE" && (
No log information available.
From ee24eb2eafbd3c0ea712a0b59a0032d2112511d9 Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Wed, 25 Mar 2026 12:18:32 +0530 Subject: [PATCH 15/26] Refactor Connection Test page use oxygen ui --- .../pages/connection-test.tsx | 789 +++++++++++------- 1 file changed, 491 insertions(+), 298 deletions(-) diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index cecc7e30ebd..c53c2ee6475 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -6,7 +6,7 @@ * in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an @@ -20,17 +20,23 @@ * Connection Test page for Identity Provider. */ +import Alert from "@oxygen-ui/react/Alert"; +import Box from "@oxygen-ui/react/Box"; +import Button from "@oxygen-ui/react/Button"; +import Card from "@oxygen-ui/react/Card"; +import CircularProgress from "@oxygen-ui/react/CircularProgress"; +import Skeleton from "@oxygen-ui/react/Skeleton"; +import Stack from "@oxygen-ui/react/Stack"; +import Tab from "@oxygen-ui/react/Tab"; +import TabPanel from "@oxygen-ui/react/TabPanel"; +import Tabs from "@oxygen-ui/react/Tabs"; +import Typography from "@oxygen-ui/react/Typography"; import { history } from "@wso2is/admin.core.v1/helpers/history"; -import { - PrimaryButton, - TabPageLayout, - ContentLoader -} from "@wso2is/react-components"; -import React, { useState, useRef, useEffect } from "react"; import useResourceEndpoints from "@wso2is/admin.core.v1/hooks/use-resource-endpoints"; +import { TabPageLayout } from "@wso2is/react-components"; +import React, { ReactElement, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { RouteComponentProps } from "react-router-dom"; -import { Header, Icon, Segment, Tab, Placeholder } from "semantic-ui-react"; /** @@ -41,6 +47,11 @@ interface RouteParams { id?: string; } +const RotateIcon = (): ReactElement => { + /* eslint-disable-next-line max-len */ + return ; +}; + /** * Connection Test page component. * @@ -91,14 +102,14 @@ const ConnectionTestPage: React.FC> = (props) = */ useEffect(() => { const state = location?.state as any; - + if (state?.debugId && state?.authorizationUrl && !autoRunTriggered) { setAutoRunTriggered(true); setDebugId(state.debugId); - + // Open the authorization URL const authPopup = window.open(state.authorizationUrl, "_blank"); - + // Monitor the popup popupInterval.current = setInterval(() => { try { @@ -120,7 +131,7 @@ const ConnectionTestPage: React.FC> = (props) = fetchResult(state.debugId); }, 30000); } - }, [location, autoRunTriggered]); + }, [ location, autoRunTriggered ]); /** @@ -129,6 +140,7 @@ const ConnectionTestPage: React.FC> = (props) = const fetchResult = async (sid: string): Promise => { if (!sid) { setError("No debug session id found..."); + return; } @@ -142,6 +154,7 @@ const ConnectionTestPage: React.FC> = (props) = `${resourceEndpoints.debug}/${sid}/result`, { withCredentials: true } ); + setResult(response.data); // eslint-disable-next-line no-console @@ -149,7 +162,7 @@ const ConnectionTestPage: React.FC> = (props) = } catch (err: any) { // eslint-disable-next-line no-console console.error("[ConnectionTest] Fetch error:", err?.response?.status, err?.message); - + if (err?.response?.status === 404) { setError("Something went wrong."); } else { @@ -174,6 +187,7 @@ const ConnectionTestPage: React.FC> = (props) = if (!connectorId) { setError("Connection ID is missing. Cannot run test."); + return; } @@ -206,7 +220,7 @@ const ConnectionTestPage: React.FC> = (props) = // Open authorizationUrl in a new tab if present if (authorizationUrl) { const authPopup = window.open(authorizationUrl, "_blank"); - + // Monitor the popup and fetch results when it closes popupInterval.current = setInterval(() => { try { @@ -255,7 +269,7 @@ const ConnectionTestPage: React.FC> = (props) = // Check for top-level FAILURE status first const topLevelFailure = result?.status === "FAILURE"; - + // Check if any step explicitly failed (not pending or success) const hasStepError = ( (steps.connectionStatus && steps.connectionStatus !== "success" && steps.connectionStatus !== "pending") || @@ -263,10 +277,10 @@ const ConnectionTestPage: React.FC> = (props) = (steps.claimMappingStatus && steps.claimMappingStatus !== "success" && steps.claimMappingStatus !== "pending") || result?.error ); - + // Check for error fields in metadata const hasErrorFields = metadataObj?.error_details || metadataObj?.error_description || metadataObj?.error_code; - + if (topLevelFailure || hasStepError || hasErrorFields) { setHasError(true); setActiveTab(2); // Switch to Logs tab (index 2) @@ -274,7 +288,7 @@ const ConnectionTestPage: React.FC> = (props) = setHasError(false); } } - }, [result]); + }, [ result ]); /** * Handles the back button click event. @@ -284,7 +298,7 @@ const ConnectionTestPage: React.FC> = (props) = const pathParts = location.pathname.split("/"); const tenantIndex = pathParts.indexOf("t"); const tenantDomain = tenantIndex !== -1 ? pathParts[tenantIndex + 1] : "carbon.super"; - + // Navigate to the specific connection page history.push(`/t/${tenantDomain}/console/connections/${connectorId}`); }; @@ -296,23 +310,64 @@ const ConnectionTestPage: React.FC> = (props) = * Renders the results tabs when test results are available. */ const renderResultsTabs = () => { + const renderCodeBlock = ( + value: unknown, + options: { + color?: string; + marginBottom?: number; + wordBreak?: "break-all" | "break-word"; + } = {} + ) => ( + + + { typeof value === "string" ? value : JSON.stringify(value, null, 2) } + + + ); + const tabPanes = [ { menuItem: "ID Token", render: () => { - // Helper to decode JWT - const decodeJWT = (token) => { - if (!token || typeof token !== "string" || token.split(".").length < 3) return null; - const [header, payload, signature] = token.split("."); - const decode = (str) => { + const decodeJWT = (token?: string) => { + if (!token || typeof token !== "string" || token.split(".").length < 3) { + return null; + } + + const [ header, payload, signature ] = token.split("."); + const decode = (value: string) => { try { - let base64 = str.replace(/-/g, "+").replace(/_/g, "/"); - while (base64.length % 4) base64 += "="; + let base64: string = value.replace(/-/g, "+").replace(/_/g, "/"); + + while (base64.length % 4) { + base64 += "="; + } + return JSON.parse(atob(base64)); } catch { return null; } }; + return { header: decode(header), payload: decode(payload), @@ -320,71 +375,43 @@ const ConnectionTestPage: React.FC> = (props) = }; }; - const idToken = result?.metadata?.idToken; + const idToken: string | undefined = result?.metadata?.idToken; const decoded = decodeJWT(idToken); return ( - -
- {decoded ? ( -
-
-
-
Header
-
-                                                    {JSON.stringify(decoded.header, null, 2)}
-                                                
-
-
-
Payload
-
-                                                    {JSON.stringify(decoded.payload, null, 2)}
-                                                
-
-
-
Signature
-
-
-                                                        {decoded.signature}
-                                                    
-
-
-
-
+ + + { decoded ? ( + + + Header + + { renderCodeBlock(decoded.header, { marginBottom: 2 }) } + + + Payload + + { renderCodeBlock(decoded.payload, { marginBottom: 2 }) } + + + Signature + + { renderCodeBlock(decoded.signature, { wordBreak: "break-all" }) } + ) : ( -
- Unable to decode token or not a valid JWT. -
- )} -
External Redirect URL
-
-                                    {typeof result?.metadata?.externalRedirectUrl === 'string' ? result.metadata.externalRedirectUrl : JSON.stringify(result?.metadata?.externalRedirectUrl, null, 2)}
-                                
-
-
+ + Unable to decode token or not a valid JWT. + + ) } + + + + External Redirect URL + + { renderCodeBlock(result?.metadata?.externalRedirectUrl) } + + + ); } }, @@ -393,49 +420,139 @@ const ConnectionTestPage: React.FC> = (props) = render: () => { const claimsArray = Array.isArray(result?.metadata?.mappedClaims) ? result?.metadata?.mappedClaims : []; const sortedClaims = claimsArray.sort((a, b) => { - if (a.status === 'Successful' && b.status !== 'Successful') return -1; - if (a.status !== 'Successful' && b.status === 'Successful') return 1; + if (a.status === "Successful" && b.status !== "Successful") { + return -1; + } + if (a.status !== "Successful" && b.status === "Successful") { + return 1; + } + return 0; }); - const formatValue = (val) => { - if (val === null || val === undefined) return '-'; - if (typeof val === 'object') return JSON.stringify(val); - return String(val); + const formatValue = (value: unknown) => { + if (value === null || value === undefined) { + return "-"; + } + if (typeof value === "object") { + return JSON.stringify(value); + } + + return String(value); }; return ( - -
-
-
- - - - - - - - - - {sortedClaims.length === 0 && ( - - - - )} - {sortedClaims.map((claim, idx) => ( - - - - - - - ))} - -
IDP ClaimIS Claim (URI)ValueMapping Status
No claims found.
{claim.idpClaim}{claim.isClaim || '-'}{formatValue(claim.value)}{claim.status}
-
-
- + + + + + + + IDP Claim + + + IS Claim (URI) + + + Value + + + Mapping Status + + + + + { sortedClaims.length === 0 && ( + + + No claims found. + + + ) } + { sortedClaims.map((claim, idx) => ( + + + { claim.idpClaim } + + + { claim.isClaim || "-" } + + + { formatValue(claim.value) } + + + { claim.status } + + + )) } + + + + ); } }, @@ -443,222 +560,298 @@ const ConnectionTestPage: React.FC> = (props) = menuItem: "Diagnostics", render: () => { const formatLogs = () => { - // Handle nested metadata structure const metadataObj = result?.metadata || {}; - - // Get steps from stepStatus or individual status fields const stepStatus = metadataObj?.stepStatus || {}; - const steps = { - connectionStatus: stepStatus?.connectionStatus || metadataObj?.connectionStatus, + const steps: Record = { authenticationStatus: stepStatus?.authenticationStatus || metadataObj?.authenticationStatus, + claimExtractionStatus: stepStatus?.claimExtractionStatus || metadataObj?.claimExtractionStatus, claimMappingStatus: stepStatus?.claimMappingStatus || metadataObj?.claimMappingStatus, - claimExtractionStatus: stepStatus?.claimExtractionStatus || metadataObj?.claimExtractionStatus + connectionStatus: stepStatus?.connectionStatus || metadataObj?.connectionStatus }; const errorDescription = metadataObj?.error_description || null; const errorCode = metadataObj?.error_code || null; const topLevelStatus = result?.status; - + return ( -
- {(errorDescription || errorCode) && ( -
-
Error Information
-
- {errorCode && ( -
- Error Code: -

- {errorCode} -

-
- )} - {errorDescription && ( -
- Description: -

- {errorDescription} -

-
- )} -
-
- )} - - {Object.keys(steps).length > 0 && ( -
-
Step Status
-
- {[ - { key: 'connectionStatus', label: 'Connection Creation' }, - { key: 'authenticationStatus', label: 'Authentication' }, - { key: 'claimMappingStatus', label: 'Claims Mapping' }, + + { (errorDescription || errorCode) && ( + + + Error Information + + { errorCode && ( + + + Error Code + + + { errorCode } + + + ) } + { errorDescription && ( + + + Description + + + { errorDescription } + + + ) } + + ) } + + { Object.keys(steps).length > 0 && ( + + + Step Status + + + { [ + { key: "connectionStatus", label: "Connection Creation" }, + { key: "authenticationStatus", label: "Authentication" }, + { key: "claimMappingStatus", label: "Claims Mapping" } ].map(({ key, label }) => ( steps[key] ? ( -
- {label}: - - {String(steps[key])} - -
+ + + { label } + + + { String(steps[key]) } + + ) : null - ))} -
-
- )} - - {!errorDescription && !errorCode && Object.keys(steps).length === 0 && topLevelStatus !== "FAILURE" && ( -
- No log information available. -
- )} -
+ )) } + + + ) } + + { !errorDescription && !errorCode && Object.keys(steps).length === 0 && topLevelStatus !== "FAILURE" && ( + + No log information available. + + ) } + ); }; return ( - -
- {formatLogs()} -
-
+ + { formatLogs() } + ); } } ]; - return setActiveTab(data.activeIndex as number)} />; + return ( + + setActiveTab(value) }> + { tabPanes.map((tabPane) => ( + + )) } + + { tabPanes.map((tabPane, index) => ( + + { tabPane.render() } + + )) } + + ); }; return ( } data-testid="idp-test-result-rerun-button" > - Rerun Test - + ) } titleTextAlign="left" - contentTopMargin={true} - bottomMargin={false} - data-testid={`${testId}-page-layout`} + contentTopMargin={ true } + bottomMargin={ false } + data-testid={ `${testId}-page-layout` } > - {/* Test Status Banner */} - {result && !error && ( - -
- -
-
- {hasError ? 'Test Failed' : 'Test Passed'} -
-

- {hasError - ? 'Some steps failed during the connection test. Check the Logs tab for details.' - : 'All connection test steps completed successfully.'} -

-
-
-
- )} - - {/* Results Section */} - {error && ( - - {error} -
- - Retry - -
-
- )} - - {!error && result && ( - + - {renderResultsTabs()} - - )} - - {!error && !result && (debugId || loading) && ( - + + + { hasError ? "Test Failed" : "Test Passed" } + + + { hasError + ? "Some steps failed during the connection test. Check the Logs tab for details." + : "All connection test steps completed successfully." } + + + + + ) } + + { /* Results Section */ } + { error && ( + + + { error } + + + + + + ) } + + { !error && result && ( + + { renderResultsTabs() } + + ) } + + { !error && !result && (debugId || loading) && ( + + + + + { loading ? "Loading test results..." : "Waiting for authentication to complete..." } + + + + + + + + + + + ) }
); }; From d72b5a753a3d2f0b8addfaa55a669d02fba8b724 Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Thu, 9 Apr 2026 15:49:46 +0530 Subject: [PATCH 16/26] Eenhance error handling and UI feedback --- .../pages/connection-test.tsx | 207 ++++++++---------- 1 file changed, 95 insertions(+), 112 deletions(-) diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index c53c2ee6475..1013a961883 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -21,6 +21,7 @@ */ import Alert from "@oxygen-ui/react/Alert"; +import AlertTitle from "@oxygen-ui/react/AlertTitle"; import Box from "@oxygen-ui/react/Box"; import Button from "@oxygen-ui/react/Button"; import Card from "@oxygen-ui/react/Card"; @@ -70,6 +71,7 @@ const ConnectionTestPage: React.FC> = (props) = const [ error, setError ] = useState(null); const [ loading, setLoading ] = useState(false); const [ hasError, setHasError ] = useState(false); + const [ hasPartial, setHasPartial ] = useState(false); const [ activeTab, setActiveTab ] = useState(0); const [ autoRunTriggered, setAutoRunTriggered ] = useState(false); @@ -157,14 +159,12 @@ const ConnectionTestPage: React.FC> = (props) = setResult(response.data); - // eslint-disable-next-line no-console - console.log("[ConnectionTest] Successfully fetched results:", response.data); } catch (err: any) { // eslint-disable-next-line no-console console.error("[ConnectionTest] Fetch error:", err?.response?.status, err?.message); if (err?.response?.status === 404) { - setError("Something went wrong."); + setError("Unable to retrieve test results. Please try running the test again."); } else { setError(err?.response?.data?.message || err?.message || "Failed to fetch debug results."); } @@ -181,9 +181,7 @@ const ConnectionTestPage: React.FC> = (props) = setError(null); setResult(null); setHasError(false); - - // eslint-disable-next-line no-console - console.log("[ConnectionTest] Starting rerun test for connectorId:", connectorId); + setHasPartial(false); if (!connectorId) { setError("Connection ID is missing. Cannot run test."); @@ -197,9 +195,6 @@ const ConnectionTestPage: React.FC> = (props) = connectionId: connectorId }; - // eslint-disable-next-line no-console - console.log("[ConnectionTest] Posting debug request with payload:", payload); - const response = await axios.post( `${resourceEndpoints.debug}/idp`, payload, @@ -209,9 +204,6 @@ const ConnectionTestPage: React.FC> = (props) = const newDebugId = response.data.debugId; const authorizationUrl = response.data.metadata.authorizationUrl; - // eslint-disable-next-line no-console - console.log("[ConnectionTest] Debug session created:", { newDebugId, authorizationUrl }); - setDebugId(newDebugId); // Clear any existing timers from previous runs @@ -260,11 +252,16 @@ const ConnectionTestPage: React.FC> = (props) = if (result) { // Handle nested metadata structure const metadataObj = result?.metadata?.metadata || result?.metadata || {}; - const steps = metadataObj?.steps || { - connectionStatus: metadataObj?.connectionStatus, - authenticationStatus: metadataObj?.authenticationStatus, - claimMappingStatus: metadataObj?.claimMappingStatus, - claimExtractionStatus: metadataObj?.claimExtractionStatus + + // Try to get steps from stepStatus directly + const stepStatus = metadataObj?.stepStatus || metadataObj?.steps || {}; + + const steps: Record = { + connectionStatus: stepStatus?.connectionStatus || metadataObj?.connectionStatus, + authenticationStatus: stepStatus?.authenticationStatus || metadataObj?.authenticationStatus, + claimMappingStatus: stepStatus?.claimMappingStatus || metadataObj?.claimMappingStatus, + claimExtractionStatus: stepStatus?.claimExtractionStatus || metadataObj?.claimExtractionStatus, + accountLinkingStatus: stepStatus?.accountLinkingStatus || metadataObj?.accountLinkingStatus }; // Check for top-level FAILURE status first @@ -274,18 +271,28 @@ const ConnectionTestPage: React.FC> = (props) = const hasStepError = ( (steps.connectionStatus && steps.connectionStatus !== "success" && steps.connectionStatus !== "pending") || (steps.authenticationStatus && steps.authenticationStatus !== "success" && steps.authenticationStatus !== "pending") || - (steps.claimMappingStatus && steps.claimMappingStatus !== "success" && steps.claimMappingStatus !== "pending") || + (steps.claimMappingStatus && steps.claimMappingStatus !== "success" && steps.claimMappingStatus !== "pending" && steps.claimMappingStatus !== "partial") || + steps.accountLinkingStatus && steps.accountLinkingStatus !== "success" && steps.accountLinkingStatus !== "pending" || result?.error ); + // Check if claim mapping is partial + const hasPartialStatus = steps.claimMappingStatus === "partial"; + // Check for error fields in metadata const hasErrorFields = metadataObj?.error_details || metadataObj?.error_description || metadataObj?.error_code; if (topLevelFailure || hasStepError || hasErrorFields) { setHasError(true); + setHasPartial(false); setActiveTab(2); // Switch to Logs tab (index 2) + } else if (hasPartialStatus) { + setHasError(false); + setHasPartial(true); + setActiveTab(2); // Switch to Logs tab (index 2) for partial status } else { setHasError(false); + setHasPartial(false); } } }, [ result ]); @@ -383,17 +390,17 @@ const ConnectionTestPage: React.FC> = (props) = { decoded ? ( - + Header { renderCodeBlock(decoded.header, { marginBottom: 2 }) } - + Payload { renderCodeBlock(decoded.payload, { marginBottom: 2 }) } - + Signature { renderCodeBlock(decoded.signature, { wordBreak: "break-all" }) } @@ -403,13 +410,6 @@ const ConnectionTestPage: React.FC> = (props) = Unable to decode token or not a valid JWT. ) } - - - - External Redirect URL - - { renderCodeBlock(result?.metadata?.externalRedirectUrl) } - ); @@ -461,24 +461,21 @@ const ConnectionTestPage: React.FC> = (props) = sx={ { backgroundColor: "#F8FAFC", borderBottom: "1px solid #E5E7EB" } } > - IDP Claim + IS Claim (URI) - IS Claim (URI) + IDP Claim Value - - Mapping Status - { sortedClaims.length === 0 && ( - - No claims found. + + No claim mappings configured. ) } @@ -487,7 +484,7 @@ const ConnectionTestPage: React.FC> = (props) = component="tr" key={ idx } sx={ { - backgroundColor: claim.status === "Successful" ? "#F0FDF4" : "#FFFFFF", + backgroundColor: "#FFFFFF", borderBottom: "1px solid #E5E7EB" } } > @@ -502,9 +499,9 @@ const ConnectionTestPage: React.FC> = (props) = p: 1, textOverflow: "ellipsis" } } - title={ claim.idpClaim } + title={ claim.isClaim || "-" } > - { claim.idpClaim } + { claim.isClaim || "-" } > = (props) = p: 1, textOverflow: "ellipsis" } } - title={ claim.isClaim || "-" } + title={ claim.idpClaim } > - { claim.isClaim || "-" } + { claim.idpClaim } > = (props) = - { claim.status } )) } @@ -557,7 +553,7 @@ const ConnectionTestPage: React.FC> = (props) = } }, { - menuItem: "Diagnostics", + menuItem: "Step Status", render: () => { const formatLogs = () => { const metadataObj = result?.metadata || {}; @@ -566,28 +562,20 @@ const ConnectionTestPage: React.FC> = (props) = authenticationStatus: stepStatus?.authenticationStatus || metadataObj?.authenticationStatus, claimExtractionStatus: stepStatus?.claimExtractionStatus || metadataObj?.claimExtractionStatus, claimMappingStatus: stepStatus?.claimMappingStatus || metadataObj?.claimMappingStatus, - connectionStatus: stepStatus?.connectionStatus || metadataObj?.connectionStatus + connectionStatus: stepStatus?.connectionStatus || metadataObj?.connectionStatus, + accountLinkingStatus: stepStatus?.accountLinkingStatus || metadataObj?.accountLinkingStatus }; const errorDescription = metadataObj?.error_description || null; const errorCode = metadataObj?.error_code || null; + const accountLinkingMessage = metadataObj?.accountLinkingMessage || null; const topLevelStatus = result?.status; + const accountLinkingFailed = steps.accountLinkingStatus === "failed"; return ( - { (errorDescription || errorCode) && ( - - - Error Information - + { (errorDescription || errorCode || (accountLinkingFailed && accountLinkingMessage)) && ( + + Error Information { errorCode && ( @@ -626,19 +614,36 @@ const ConnectionTestPage: React.FC> = (props) = ) } - + { accountLinkingFailed && accountLinkingMessage && ( + + + Account Linking Error + + + { accountLinkingMessage } + + + ) } + ) } { Object.keys(steps).length > 0 && ( - - Step Status - { [ { key: "connectionStatus", label: "Connection Creation" }, { key: "authenticationStatus", label: "Authentication" }, - { key: "claimMappingStatus", label: "Claims Mapping" } + { key: "claimMappingStatus", label: "Claim Mappings" }, + { key: "accountLinkingStatus", label: "Account Linking" }, ].map(({ key, label }) => ( steps[key] ? ( > = (props) = sx={ { backgroundColor: steps[key] === "success" ? "#E8F5E9" - : steps[key] === "failed" || steps[key] === "error" - ? "#FEE4E2" - : "#F3F4F6", + : steps[key] === "partial" + ? "#FFFBEB" + : steps[key] === "failed" || steps[key] === "error" + ? "#FEE4E2" + : "#F3F4F6", borderRadius: 1, color: steps[key] === "success" ? "#15803D" - : steps[key] === "failed" || steps[key] === "error" - ? "#B42318" - : "text.secondary", + : steps[key] === "partial" + ? "#B45309" + : steps[key] === "failed" || steps[key] === "error" + ? "#B42318" + : "text.secondary", + display: "inline-block", fontFamily: "monospace", fontSize: 13, fontWeight: 500, + lineHeight: 1, + minWidth: 60, px: 1, py: 0.5, + textAlign: "center", textTransform: "capitalize" } } > @@ -753,48 +766,17 @@ const ConnectionTestPage: React.FC> = (props) = > { /* Test Status Banner */ } { result && !error && ( - - - - { hasError ? "!" : "OK" } - - - - { hasError ? "Test Failed" : "Test Passed" } - - - { hasError - ? "Some steps failed during the connection test. Check the Logs tab for details." - : "All connection test steps completed successfully." } - - - + + + + { hasError ? "Test Failed" : hasPartial ? "Test Passed with Warnings" : "Test Passed" } + + { hasError + ? "Some steps failed during the connection test. Check the Diagnostic Logs tab for details." + : hasPartial + ? "Test passed but some claims were not successfully mapped. Check the Claim Mappings tab for details." + : "All connection test steps completed successfully." } + ) } @@ -806,6 +788,7 @@ const ConnectionTestPage: React.FC> = (props) = data-testid="idp-test-result-error" > + Error { error } From 118393a373a3ba6e329bf35aedc522518aacec80 Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Mon, 11 May 2026 12:43:28 +0530 Subject: [PATCH 17/26] Address UI review comments --- .../pages/connection-test.tsx | 616 ++++++++++++------ .../src/main/webapp/debugSuccess.jsp | 152 ++++- 2 files changed, 574 insertions(+), 194 deletions(-) diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index 1013a961883..8e3b6c22409 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -38,6 +38,7 @@ import { TabPageLayout } from "@wso2is/react-components"; import React, { ReactElement, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { RouteComponentProps } from "react-router-dom"; +import { Accordion, Icon } from "semantic-ui-react"; /** @@ -62,22 +63,69 @@ const RotateIcon = (): ReactElement => { const ConnectionTestPage: React.FC> = (props) => { const { id: connectorId = "" } = props.match.params || {}; const { location } = props; + const resultCacheKey = `idp-test-result:${connectorId}`; + const locationState = location?.state as any; + const hasPendingAutoRun = Boolean(locationState?.debugId && locationState?.authorizationUrl); + + const getCachedResult = (): any => { + if (typeof window === "undefined" || !connectorId) { + return null; + } + + const cachedResult = window.localStorage.getItem(resultCacheKey); + + if (!cachedResult) { + return null; + } + + try { + return JSON.parse(cachedResult); + } catch (e) { + window.localStorage.removeItem(resultCacheKey); + + return null; + } + }; const { t } = useTranslation(); const { resourceEndpoints } = useResourceEndpoints(); const [ debugId, setDebugId ] = useState(null); - const [ result, setResult ] = useState(null); + const [ result, setResult ] = useState(() => hasPendingAutoRun ? null : getCachedResult()); const [ error, setError ] = useState(null); const [ loading, setLoading ] = useState(false); const [ hasError, setHasError ] = useState(false); const [ hasPartial, setHasPartial ] = useState(false); const [ activeTab, setActiveTab ] = useState(0); const [ autoRunTriggered, setAutoRunTriggered ] = useState(false); + const [ isStatusBannerVisible, setIsStatusBannerVisible ] = useState(true); + const [ expandedDiagnosticLogs, setExpandedDiagnosticLogs ] = useState([]); const popupInterval = useRef(null); const fetchTimer = useRef(null); + /** + * Clears cached test result for the current connection. + */ + const clearCachedResult = (): void => { + if (typeof window === "undefined" || !connectorId) { + return; + } + + window.localStorage.removeItem(resultCacheKey); + }; + + /** + * Saves latest test result payload to browser cache. + */ + const cacheResult = (value: any): void => { + if (typeof window === "undefined" || !connectorId) { + return; + } + + window.localStorage.setItem(resultCacheKey, JSON.stringify(value)); + }; + /** * Clears all running timers for popup monitoring and result fetching. */ @@ -103,9 +151,18 @@ const ConnectionTestPage: React.FC> = (props) = * Auto-run test if debugId and authUrl are provided via location state. */ useEffect(() => { - const state = location?.state as any; + const state = locationState; if (state?.debugId && state?.authorizationUrl && !autoRunTriggered) { + // Reset stale UI/cached data before starting an auto-run flow. + setResult(null); + setError(null); + setHasError(false); + setHasPartial(false); + setIsStatusBannerVisible(true); + setExpandedDiagnosticLogs([]); + clearCachedResult(); + setAutoRunTriggered(true); setDebugId(state.debugId); @@ -133,7 +190,7 @@ const ConnectionTestPage: React.FC> = (props) = fetchResult(state.debugId); }, 30000); } - }, [ location, autoRunTriggered ]); + }, [ locationState, autoRunTriggered ]); /** @@ -149,6 +206,7 @@ const ConnectionTestPage: React.FC> = (props) = setLoading(true); setError(null); setResult(null); + setExpandedDiagnosticLogs([]); try { const axios = (await import("axios")).default; @@ -157,14 +215,18 @@ const ConnectionTestPage: React.FC> = (props) = { withCredentials: true } ); + clearCachedResult(); setResult(response.data); + setIsStatusBannerVisible(true); + cacheResult(response.data); + setExpandedDiagnosticLogs([]); } catch (err: any) { // eslint-disable-next-line no-console console.error("[ConnectionTest] Fetch error:", err?.response?.status, err?.message); if (err?.response?.status === 404) { - setError("Unable to retrieve test results. Please try running the test again."); + setError("Unable to retrieve test results for this session."); } else { setError(err?.response?.data?.message || err?.message || "Failed to fetch debug results."); } @@ -182,6 +244,9 @@ const ConnectionTestPage: React.FC> = (props) = setResult(null); setHasError(false); setHasPartial(false); + setIsStatusBannerVisible(true); + setExpandedDiagnosticLogs([]); + clearCachedResult(); if (!connectorId) { setError("Connection ID is missing. Cannot run test."); @@ -257,7 +322,7 @@ const ConnectionTestPage: React.FC> = (props) = const stepStatus = metadataObj?.stepStatus || metadataObj?.steps || {}; const steps: Record = { - connectionStatus: stepStatus?.connectionStatus || metadataObj?.connectionStatus, + connectionCreation: stepStatus?.connectionCreation || metadataObj?.connectionCreation, authenticationStatus: stepStatus?.authenticationStatus || metadataObj?.authenticationStatus, claimMappingStatus: stepStatus?.claimMappingStatus || metadataObj?.claimMappingStatus, claimExtractionStatus: stepStatus?.claimExtractionStatus || metadataObj?.claimExtractionStatus, @@ -269,7 +334,7 @@ const ConnectionTestPage: React.FC> = (props) = // Check if any step explicitly failed (not pending or success) const hasStepError = ( - (steps.connectionStatus && steps.connectionStatus !== "success" && steps.connectionStatus !== "pending") || + (steps.connectionCreation && steps.connectionCreation !== "success" && steps.connectionCreation !== "pending") || (steps.authenticationStatus && steps.authenticationStatus !== "success" && steps.authenticationStatus !== "pending") || (steps.claimMappingStatus && steps.claimMappingStatus !== "success" && steps.claimMappingStatus !== "pending" && steps.claimMappingStatus !== "partial") || steps.accountLinkingStatus && steps.accountLinkingStatus !== "success" && steps.accountLinkingStatus !== "pending" || @@ -353,6 +418,7 @@ const ConnectionTestPage: React.FC> = (props) = const tabPanes = [ { + key: "id-token", menuItem: "ID Token", render: () => { const decodeJWT = (token?: string) => { @@ -416,6 +482,7 @@ const ConnectionTestPage: React.FC> = (props) = } }, { + key: "claim-mappings", menuItem: "Claim Mappings", render: () => { const claimsArray = Array.isArray(result?.metadata?.mappedClaims) ? result?.metadata?.mappedClaims : []; @@ -460,13 +527,43 @@ const ConnectionTestPage: React.FC> = (props) = component="tr" sx={ { backgroundColor: "#F8FAFC", borderBottom: "1px solid #E5E7EB" } } > - - IS Claim (URI) + + Local Claim (URI) - + IDP Claim - + Value @@ -494,14 +591,14 @@ const ConnectionTestPage: React.FC> = (props) = borderRight: "1px solid #E5E7EB", fontFamily: "monospace", fontSize: 13, - maxWidth: 200, - overflow: "hidden", + maxWidth: 360, p: 1, - textOverflow: "ellipsis" + whiteSpace: "normal", + wordBreak: "break-word" } } - title={ claim.isClaim || "-" } + title={ claim.localClaim || claim.isClaim || "-" } > - { claim.isClaim || "-" } + { claim.localClaim || claim.isClaim || "-" } > = (props) = borderRight: "1px solid #E5E7EB", fontFamily: "monospace", fontSize: 13, - maxWidth: 200, + maxWidth: 180, overflow: "hidden", p: 1, textOverflow: "ellipsis" @@ -525,7 +622,7 @@ const ConnectionTestPage: React.FC> = (props) = color: "text.secondary", fontFamily: "monospace", fontSize: 13, - maxWidth: 250, + maxWidth: 200, overflow: "hidden", p: 1, textOverflow: "ellipsis" @@ -534,15 +631,6 @@ const ConnectionTestPage: React.FC> = (props) = > { formatValue(claim.value) } - - )) } @@ -553,16 +641,22 @@ const ConnectionTestPage: React.FC> = (props) = } }, { - menuItem: "Step Status", + key: "diagnosis", + menuItem: "Diagnosis", render: () => { const formatLogs = () => { const metadataObj = result?.metadata || {}; const stepStatus = metadataObj?.stepStatus || {}; + const allDiagnostics = Array.isArray(metadataObj?.diagnostics) ? metadataObj.diagnostics : []; + // Filter out 'started' status entries and claim validation stage entries + const diagnostics = allDiagnostics.filter((log: any) => + !(log.status === "started" || log.stage === "claimValidation") + ); const steps: Record = { authenticationStatus: stepStatus?.authenticationStatus || metadataObj?.authenticationStatus, claimExtractionStatus: stepStatus?.claimExtractionStatus || metadataObj?.claimExtractionStatus, claimMappingStatus: stepStatus?.claimMappingStatus || metadataObj?.claimMappingStatus, - connectionStatus: stepStatus?.connectionStatus || metadataObj?.connectionStatus, + connectionCreation: stepStatus?.connectionCreation || metadataObj?.connectionCreation, accountLinkingStatus: stepStatus?.accountLinkingStatus || metadataObj?.accountLinkingStatus }; const errorDescription = metadataObj?.error_description || null; @@ -570,142 +664,285 @@ const ConnectionTestPage: React.FC> = (props) = const accountLinkingMessage = metadataObj?.accountLinkingMessage || null; const topLevelStatus = result?.status; const accountLinkingFailed = steps.accountLinkingStatus === "failed"; + const getStepStatusPalette = (status?: string) => { + const normalizedStatus = String(status || "unknown").toLowerCase(); + + switch (normalizedStatus) { + case "success": + return { background: "#E8F5E9", color: "#15803D" }; + case "partial": + return { background: "#FFFBEB", color: "#B45309" }; + case "started": + case "pending": + return { background: "#E0F2FE", color: "#0284C7" }; + case "failed": + case "error": + return { background: "#FEE4E2", color: "#B42318" }; + default: + return { background: "#F3F4F6", color: "#6B7280" }; + } + }; + const getDiagnosticStatusIcon = (status?: string): ReactElement => { + let iconName: string; + let iconColor: "green" | "yellow" | "red"; + + switch (String(status || "").toUpperCase()) { + case "SUCCESS": + iconName = "check circle"; + iconColor = "green"; + break; + case "PARTIAL": + iconName = "check circle"; + iconColor = "yellow"; + break; + case "FAILED": + case "ERROR": + iconName = "times circle"; + iconColor = "red"; + break; + default: + return <>; + } + + return ( + + + + ); + }; + const toggleDiagnosticLog = (index: number) => { + setExpandedDiagnosticLogs((previous: number[]) => ( + previous.includes(index) + ? previous.filter((item: number) => item !== index) + : [ ...previous, index ] + )); + }; + const renderDiagnosticLogValue = (value: unknown) => { + if (value === null || value === undefined || value === "") { + return "-"; + } + + if (typeof value === "object") { + return ( + + { JSON.stringify(value, null, 2) } + + ); + } + + return String(value); + }; + const renderExpandedDiagnosticRows = (log: any) => { + const rows: Array<{ label: string; value: unknown }> = [ + { + label: "recordedAt", + value: log.timestamp ? new Date(log.timestamp).toLocaleString() : null + }, + { label: "resultStatus", value: log.status }, + { label: "details", value: log.details }, + { label: "errorCode", value: log.errorCode }, + { label: "federatedAttribute", value: log.federatedAttribute }, + { label: "errorDescription", value: log.errorDescription }, + ]; + + return rows.filter((row) => row.value !== undefined && row.value !== null && row.value !== ""); + }; return ( - { (errorDescription || errorCode || (accountLinkingFailed && accountLinkingMessage)) && ( - - Error Information - { errorCode && ( - - - Error Code - - - { errorCode } - - - ) } - { errorDescription && ( - - - Description - - - { errorDescription } - - - ) } - { accountLinkingFailed && accountLinkingMessage && ( - - - Account Linking Error - - - { accountLinkingMessage } - - - ) } + + + { !errorDescription && !errorCode && Object.keys(steps).length === 0 && topLevelStatus !== "FAILURE" && ( + + No log information available. ) } - { Object.keys(steps).length > 0 && ( - - - { [ - { key: "connectionStatus", label: "Connection Creation" }, - { key: "authenticationStatus", label: "Authentication" }, - { key: "claimMappingStatus", label: "Claim Mappings" }, - { key: "accountLinkingStatus", label: "Account Linking" }, - ].map(({ key, label }) => ( - steps[key] ? ( - - - { label } - + { diagnostics.length > 0 && ( + + + + { diagnostics.map((log: any, idx: number) => { + const timestamp = log.timestamp + ? new Date(log.timestamp).toLocaleString() + : "Timestamp unavailable"; + const isExpanded = expandedDiagnosticLogs.includes(idx); + + return ( - { String(steps[key]) } + toggleDiagnosticLog(idx) } + style={ { padding: 0 } } + > + + + + + + { getDiagnosticStatusIcon(log.status) } + + { log.message || "No diagnostic message provided." } + + + + { timestamp } + + + + + + + + { renderExpandedDiagnosticRows(log).map((row) => ( + + + { row.label }: + + + { renderDiagnosticLogValue(row.value) } + + + )) } + + + + - - ) : null - )) } - - - ) } - - { !errorDescription && !errorCode && Object.keys(steps).length === 0 && topLevelStatus !== "FAILURE" && ( - - No log information available. - + ); + }) } + + + ) } ); @@ -724,11 +961,11 @@ const ConnectionTestPage: React.FC> = (props) = setActiveTab(value) }> { tabPanes.map((tabPane) => ( - + )) } { tabPanes.map((tabPane, index) => ( - + { tabPane.render() } )) } @@ -737,42 +974,46 @@ const ConnectionTestPage: React.FC> = (props) = }; return ( - } - data-testid="idp-test-result-rerun-button" - > - Rerun Test - ) - } - titleTextAlign="left" - contentTopMargin={ true } - bottomMargin={ false } - data-testid={ `${testId}-page-layout` } - > +
+ } + data-testid="idp-test-result-rerun-button" + > + Rerun Test + ) + } + titleTextAlign="left" + contentTopMargin={ true } + bottomMargin={ false } + data-testid={ `${testId}-page-layout` } + > { /* Test Status Banner */ } - { result && !error && ( + { result && !error && isStatusBannerVisible && ( - + setIsStatusBannerVisible(false) } + > - { hasError ? "Test Failed" : hasPartial ? "Test Passed with Warnings" : "Test Passed" } + { hasError ? "Test Failed" : hasPartial ? "Test Passed Partially" : "Test Passed" } { hasError - ? "Some steps failed during the connection test. Check the Diagnostic Logs tab for details." + ? "Some steps failed during the connection test. Check the Diagnosis tab for details." : hasPartial ? "Test passed but some claims were not successfully mapped. Check the Claim Mappings tab for details." : "All connection test steps completed successfully." } @@ -835,7 +1076,8 @@ const ConnectionTestPage: React.FC> = (props) = ) } - + +
); }; diff --git a/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp index 503272fb8db..59991578ead 100644 --- a/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp +++ b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp @@ -66,13 +66,15 @@ <% } %> -
-

- <%=AuthenticationEndpointUtil.i18n(resourceBundle, "Authentication Successful")%> -

-

- <%=AuthenticationEndpointUtil.i18n(resourceBundle, "You will be redirected shortly.")%> -

+
+
+
+

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "Authentication Successful")%>

+
+
+ <%=AuthenticationEndpointUtil.i18n(resourceBundle, "You will be redirected shortly.")%> +
+
@@ -114,3 +116,139 @@ +<%-- + ~ Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + ~ + ~ WSO2 LLC. licenses this file to you under the Apache License, + ~ Version 2.0 (the "License"); you may not use this file except + ~ in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. +--%> + +<%@ page import="java.io.File" %> +<%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.AuthenticationEndpointUtil" %> +<%@ page import="org.owasp.encoder.Encode" %> +<%@ page import="org.apache.commons.text.StringEscapeUtils" %> +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> +<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> + +<%@ include file="includes/localize.jsp" %> + +<%-- Include tenant context --%> + + +<%-- Branding Preferences --%> + + +<% request.setAttribute("pageName","debug-success"); %> + +<%-- Data for the layout from the page --%> +<% + layoutData.put("isResponsePage", true); + layoutData.put("isSuccessResponse", true); + layoutData.put("isDebugSuccessPage", true); +%> + + + + + <%-- header --%> + <% + File headerFile = new File(getServletContext().getRealPath("extensions/header.jsp")); + if (headerFile.exists()) { + %> + + <% } else { %> + + <% } %> + +" style="--orange-gradient: radial-gradient(circle at 50% 50%, rgba(255, 165, 0, 0.18521823) 0, #f5f6f6 40%, #f5f6f6 100%);"> + + + + <%-- product-title --%> + <% + File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); + if (productTitleFile.exists()) { + %> + + <% } else { %> + + <% } %> + + +
+
+
+

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "Debug Request was Completed")%>

+
+
+ <%= AuthenticationEndpointUtil.i18n(resourceBundle, "You will be redirected to results page in ") %> + 2 s +
+
+
+
+ + <%-- product-footer --%> + <% + File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); + if (productFooterFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + +
+ + <%-- footer --%> + <% + File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); + if (footerFile.exists()) { + %> + + <% } else { %> + + <% } %> + + + + From 51bb1d1bf7617ff6d0cf68c89dc8edf2f721ec2f Mon Sep 17 00:00:00 2001 From: LinukaAR Date: Thu, 14 May 2026 12:12:41 +0530 Subject: [PATCH 18/26] Address coderabbit comments --- apps/console/src/configs/routes.tsx | 2 +- .../api/connection-test-api.ts | 117 +++++ .../components/connection-test-styles.tsx | 147 +++++++ .../admin.connections.v1/configs/endpoints.ts | 2 +- .../models/connection-test.ts | 65 +++ .../admin.connections.v1/models/connection.ts | 8 + .../pages/connection-edit.tsx | 54 ++- .../pages/connection-test.tsx | 405 +++++++----------- .../src/main/webapp/debugError.jsp | 46 +- .../src/main/webapp/debugSuccess.jsp | 127 +----- 10 files changed, 559 insertions(+), 414 deletions(-) create mode 100644 features/admin.connections.v1/api/connection-test-api.ts create mode 100644 features/admin.connections.v1/components/connection-test-styles.tsx create mode 100644 features/admin.connections.v1/models/connection-test.ts diff --git a/apps/console/src/configs/routes.tsx b/apps/console/src/configs/routes.tsx index b8e6f2f9d3b..7b556a52776 100644 --- a/apps/console/src/configs/routes.tsx +++ b/apps/console/src/configs/routes.tsx @@ -629,7 +629,7 @@ export const getAppViewRoutes = (): RouteInterface[] => { } ], component: lazy(() => import("@wso2is/admin.connections.v1/pages/connections")), - exact: false, + exact: true, icon: { icon: }, diff --git a/features/admin.connections.v1/api/connection-test-api.ts b/features/admin.connections.v1/api/connection-test-api.ts new file mode 100644 index 00000000000..155eaff373f --- /dev/null +++ b/features/admin.connections.v1/api/connection-test-api.ts @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2023-2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AsgardeoSPAClient, HttpClientInstance } from "@asgardeo/auth-react"; +import { ResourceEndpointsInterface } from "@wso2is/admin.core.v1/models/config"; +import { HttpMethods } from "@wso2is/core/models"; +import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios"; + +const httpClient: HttpClientInstance = AsgardeoSPAClient.getInstance() + .httpRequest.bind(AsgardeoSPAClient.getInstance()) + .bind(AsgardeoSPAClient.getInstance()); + +interface ConnectionTestErrorResponseInterface { + description?: string; + message?: string; +} + +interface ConnectionTestMetadataInterface { + authorizationUrl?: string; + [ key: string ]: unknown; +} + +export interface ConnectionTestSessionResponseInterface { + debugId?: string; + metadata?: ConnectionTestMetadataInterface; + [ key: string ]: unknown; +} + +/** + * Starts a connection test session. + * + * @param resourceEndpoints - Connection resource endpoints. + * @param connectionId - Connection id. + * @returns Response containing test session data. + */ +export const startConnectionTestSession = ( + resourceEndpoints: ResourceEndpointsInterface, + connectionId: string +): Promise> => { + const requestConfig: AxiosRequestConfig = { + data: { + connectionId + }, + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.POST, + url: `${ resourceEndpoints.debug }/idp` + }; + + return httpClient(requestConfig) as Promise>; +}; + +/** + * Retrieves connection test results. + * + * @param resourceEndpoints - Connection resource endpoints. + * @param sid - Debug session id. + * @returns Test result payload. + */ +export const getConnectionTestResult = ( + resourceEndpoints: ResourceEndpointsInterface, + sid: string +): Promise> => { + const requestConfig: AxiosRequestConfig = { + headers: { + "Accept": "application/json" + }, + method: HttpMethods.GET, + url: resourceEndpoints.debugResult.replace(":sid", sid) + }; + + return httpClient(requestConfig) as Promise>; +}; + +const isConnectionTestAxiosError = ( + error: unknown +): error is AxiosError => { + const candidate: AxiosError = + error as AxiosError; + + return Boolean(candidate && typeof candidate === "object" && "isAxiosError" in candidate); +}; + +/** + * Resolves a user-friendly error message from test API failures. + * + * @param error - Unknown error. + * @returns Error message if available. + */ +export const resolveConnectionTestErrorMessage = (error: unknown): string | undefined => { + if (isConnectionTestAxiosError(error)) { + return error.response?.data?.message ?? error.response?.data?.description ?? error.message; + } + + if (error instanceof Error) { + return error.message; + } + + return undefined; +}; diff --git a/features/admin.connections.v1/components/connection-test-styles.tsx b/features/admin.connections.v1/components/connection-test-styles.tsx new file mode 100644 index 00000000000..9f7660e032d --- /dev/null +++ b/features/admin.connections.v1/components/connection-test-styles.tsx @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Box from "@oxygen-ui/react/Box"; +import Card from "@oxygen-ui/react/Card"; +import { Theme, styled } from "@mui/material/styles"; + +export const StyledCodeBlockContainer = styled(Box)(({ theme }: { theme: Theme }) => ({ + background: theme.palette.background.paper, + border: `1px solid ${ theme.palette.divider }`, + borderRadius: theme.shape.borderRadius, + overflowX: "auto" +})); + +export const StyledCodeBlock = styled(Box)(({ theme }: { theme: Theme }) => ({ + color: theme.palette.text.primary, + fontFamily: "monospace", + fontSize: 13, + margin: 0, + padding: theme.spacing(1.5), + whiteSpace: "pre-wrap", + wordBreak: "break-word" +})); + +export const StyledResultCard = styled(Card)(({ theme }: { theme: Theme }) => ({ + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(3) +})); + +export const StyledTableWrapper = styled(Box)(({ theme }: { theme: Theme }) => ({ + backgroundColor: theme.palette.background.paper, + border: `1px solid ${ theme.palette.divider }`, + borderRadius: theme.shape.borderRadius, + overflow: "hidden" +})); + +export const StyledTableHeaderRow = styled(Box)(({ theme }: { theme: Theme }) => ({ + backgroundColor: theme.palette.action.hover, + borderBottom: `1px solid ${ theme.palette.divider }` +})); + +export const StyledTableRow = styled(Box)(({ theme }: { theme: Theme }) => ({ + backgroundColor: theme.palette.background.paper, + borderBottom: `1px solid ${ theme.palette.divider }` +})); + +export const StyledHeaderCell = styled(Box)(({ theme }: { theme: Theme }) => ({ + borderRight: `1px solid ${ theme.palette.divider }`, + fontSize: 15, + fontWeight: 600, + padding: theme.spacing(1.25, 1), + textAlign: "left" +})); + +export const StyledMonoCell = styled(Box)(({ theme }: { theme: Theme }) => ({ + borderRight: `1px solid ${ theme.palette.divider }`, + fontFamily: "monospace", + fontSize: 13, + padding: theme.spacing(1) +})); + +export const StyledValueCell = styled(StyledMonoCell)(({ theme }: { theme: Theme }) => ({ + color: theme.palette.text.secondary +})); + +export const StyledDiagnosticValueBlock = styled(Box)(({ theme }: { theme: Theme }) => ({ + backgroundColor: theme.palette.action.hover, + border: `1px solid ${ theme.palette.divider }`, + borderRadius: theme.shape.borderRadius, + fontFamily: "monospace", + fontSize: 12, + margin: 0, + overflowX: "auto", + padding: theme.spacing(1), + whiteSpace: "pre-wrap", + wordBreak: "break-word" +})); + +export const StyledDiagnosticsScroller = styled(Box)(({ theme }: { theme: Theme }) => ({ + maxHeight: theme.spacing(65), + overflowY: "auto", + scrollbarColor: `${ theme.palette.grey[400] } ${ theme.palette.action.hover }`, + scrollbarWidth: "thin", + "&::-webkit-scrollbar": { + width: 10 + }, + "&::-webkit-scrollbar-thumb": { + backgroundColor: theme.palette.grey[400], + border: `2px solid ${ theme.palette.action.hover }`, + borderRadius: 999 + }, + "&::-webkit-scrollbar-track": { + backgroundColor: theme.palette.action.hover, + borderRadius: 999 + } +})); + +export const StyledDiagnosticAccordionRow = styled(Box)(({ theme }: { theme: Theme }) => ({ + borderBottom: `1px solid ${ theme.palette.divider }` +})); + +export const StyledDiagnosticAccordionTitle = styled(Box)(({ theme }: { theme: Theme }) => ({ + alignItems: "center", + cursor: "pointer", + display: "flex", + flexDirection: "row", + gap: theme.spacing(1.5), + padding: theme.spacing(1.75, 1.5) +})); + +export const StyledExpandedDetailsTable = styled(Box)(({ theme }: { theme: Theme }) => ({ + borderCollapse: "collapse", + width: "100%", + "& td": { + borderTop: `1px solid ${ theme.palette.divider }`, + padding: theme.spacing(1, 1.5), + verticalAlign: "top" + } +})); + +export const StyledExpandedLabelCell = styled(Box)(({ theme }: { theme: Theme }) => ({ + color: theme.palette.text.secondary, + fontFamily: "monospace", + fontSize: 12, + width: 160 +})); + +export const StyledExpandedValueCell = styled(Box)(({ theme }: { theme: Theme }) => ({ + color: theme.palette.text.primary, + fontSize: 13 +})); diff --git a/features/admin.connections.v1/configs/endpoints.ts b/features/admin.connections.v1/configs/endpoints.ts index 222a3bcca84..7d3a1a3ee90 100644 --- a/features/admin.connections.v1/configs/endpoints.ts +++ b/features/admin.connections.v1/configs/endpoints.ts @@ -39,6 +39,6 @@ export const getConnectionResourceEndpoints = (serverHost: string): ConnectionRe CommonAuthenticatorConstants.MFA_CONNECTOR_CATEGORY_ID }`, debug: `${ serverHost }/api/server/v1/debug`, - debugResult: `${ serverHost }/api/server/v1/debug` + debugResult: `${ serverHost }/api/server/v1/debug/:sid/result` }; }; diff --git a/features/admin.connections.v1/models/connection-test.ts b/features/admin.connections.v1/models/connection-test.ts new file mode 100644 index 00000000000..c29529598e4 --- /dev/null +++ b/features/admin.connections.v1/models/connection-test.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MappedClaimInterface } from "./connection"; + +export interface ConnectionTestStepStatusInterface { + accountLinkingStatus?: string; + authenticationStatus?: string; + claimExtractionStatus?: string; + claimMappingStatus?: string; + connectionCreation?: string; +} + +export interface ConnectionTestDiagnosticLogInterface { + details?: unknown; + errorCode?: unknown; + errorDescription?: unknown; + federatedAttribute?: unknown; + message?: string; + stage?: string; + status?: string; + timestamp?: number | string; + [ key: string ]: unknown; +} + +export interface ConnectionTestResultMetadataInterface { + accountLinkingMessage?: string; + accountLinkingStatus?: string; + authenticationStatus?: string; + claimExtractionStatus?: string; + claimMappingStatus?: string; + connectionCreation?: string; + diagnostics?: ConnectionTestDiagnosticLogInterface[]; + error_code?: string; + error_description?: string; + error_details?: unknown; + idToken?: string; + mappedClaims?: MappedClaimInterface[]; + metadata?: ConnectionTestResultMetadataInterface; + stepStatus?: ConnectionTestStepStatusInterface; + steps?: ConnectionTestStepStatusInterface; + [ key: string ]: unknown; +} + +export interface ConnectionTestResultInterface { + error?: unknown; + metadata?: ConnectionTestResultMetadataInterface; + status?: string; + [ key: string ]: unknown; +} diff --git a/features/admin.connections.v1/models/connection.ts b/features/admin.connections.v1/models/connection.ts index 82046ad77c7..c6e5e06be88 100644 --- a/features/admin.connections.v1/models/connection.ts +++ b/features/admin.connections.v1/models/connection.ts @@ -118,6 +118,14 @@ export interface ConnectionCommonClaimMappingInterface { claim: ConnectionClaimInterface; } +export interface MappedClaimInterface { + idpClaim?: string; + isClaim?: string; + localClaim?: string; + status?: string; + value?: unknown; +} + /** * Captures the connection claim details. */ diff --git a/features/admin.connections.v1/pages/connection-edit.tsx b/features/admin.connections.v1/pages/connection-edit.tsx index 4e62abba7c0..fc9384fb7e9 100644 --- a/features/admin.connections.v1/pages/connection-edit.tsx +++ b/features/admin.connections.v1/pages/connection-edit.tsx @@ -20,8 +20,8 @@ import { FeatureAccessConfigInterface, useRequiredScopes } from "@wso2is/access- import { ApplicationTemplateConstants } from "@wso2is/admin.application-templates.v1/constants/templates"; import { AppConstants } from "@wso2is/admin.core.v1/constants/app-constants"; import { history } from "@wso2is/admin.core.v1/helpers/history"; -import useUIConfig from "@wso2is/admin.core.v1/hooks/use-ui-configs"; import useResourceEndpoints from "@wso2is/admin.core.v1/hooks/use-resource-endpoints"; +import useUIConfig from "@wso2is/admin.core.v1/hooks/use-ui-configs"; import { FeatureConfigInterface } from "@wso2is/admin.core.v1/models/config"; import { AppState } from "@wso2is/admin.core.v1/store"; import { @@ -39,10 +39,11 @@ import { DocumentationLink, LabelWithPopup, Popup, + SecondaryButton, TabPageLayout, useDocumentation } from "@wso2is/react-components"; -import { AxiosError } from "axios"; +import { AxiosError, AxiosResponse } from "axios"; import get from "lodash-es/get"; import orderBy from "lodash-es/orderBy"; import React, { Fragment, FunctionComponent, ReactElement, ReactNode, useEffect, useRef, useState } from "react"; @@ -50,8 +51,13 @@ import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { RouteComponentProps } from "react-router"; import { Dispatch } from "redux"; -import { Label } from "semantic-ui-react"; +import { Icon, Label } from "semantic-ui-react"; import { getLocalAuthenticator, getMultiFactorAuthenticatorDetails } from "../api/authenticators"; +import { + ConnectionTestSessionResponseInterface, + resolveConnectionTestErrorMessage, + startConnectionTestSession +} from "../api/connection-test-api"; import { getConnectionDetails, getConnectionMetaData, getConnectionTemplates } from "../api/connections"; import { EditConnection } from "../components/edit/connection-edit"; import { EditMultiFactorAuthenticator } from "../components/edit/edit-multi-factor-authenticator"; @@ -67,10 +73,6 @@ import { CustomAuthConnectionInterface, SupportedQuickStartTemplateTypes } from "../models/connection"; -import { Icon } from "semantic-ui-react"; -import { - PrimaryButton, SecondaryButton -} from "@wso2is/react-components"; import { ConnectionTemplateManagementUtils } from "../utils/connection-template-utils"; import { ConnectionsManagementUtils, handleGetConnectionsMetaDataError } from "../utils/connection-utils"; @@ -697,7 +699,7 @@ const ConnectionEditPage: FunctionComponent = if (!connector?.id) { dispatch( addAlert({ - description: "Connection ID is not available.", + description: t("authenticationProvider:notifications.getIDP.genericError.description"), level: AlertLevels.ERROR, message: t("authenticationProvider:notifications.getIDP.error.message") }) @@ -707,48 +709,40 @@ const ConnectionEditPage: FunctionComponent = } setIsTestingConnection(true); + const authPopup: Window | null = window.open("", "_blank"); try { - const axios = (await import("axios")).default; - - const response = await axios.post( - `${resourceEndpoints.debug}/idp`, - { connectionId: connector.id }, - { withCredentials: true } - ); + const response: AxiosResponse = + await startConnectionTestSession(resourceEndpoints, connector.id); - const debugId = response.data?.debugId; - const authorizationUrl = response.data?.metadata?.authorizationUrl; + const authorizationUrl: string | undefined = response.data?.metadata?.authorizationUrl; + const debugId: string | undefined = response.data?.debugId; if (debugId && authorizationUrl) { - // Navigate to test page with debug session data - const pathParts = location.pathname.split("/"); - const tenantIndex = pathParts.indexOf("t"); - const tenantDomain = tenantIndex !== -1 ? pathParts[tenantIndex + 1] : "carbon.super"; - + authPopup?.location.assign(authorizationUrl); history.push({ - pathname: `/t/${tenantDomain}/console/connections/${connector.id}/test`, + pathname: AppConstants.getPaths().get("IDP_TEST").replace(":id", connector.id), state: { - debugId, - authorizationUrl + debugId } }); } else { + authPopup?.close(); // Handle case where debug session data is incomplete dispatch( addAlert({ - description: t("authenticationProvider:notifications.getIDP.error.description", - { description: "Debug session data is incomplete." }), + description: t("authenticationProvider:notifications.getIDP.genericError.description"), level: AlertLevels.ERROR, message: t("authenticationProvider:notifications.getIDP.error.message") }) ); } - } catch (error: any) { + } catch (error: unknown) { + authPopup?.close(); dispatch( addAlert({ - description: error?.response?.data?.message || error?.response?.data?.description || - error?.message || t("authenticationProvider:notifications.getIDP.genericError.description"), + description: resolveConnectionTestErrorMessage(error) + ?? t("authenticationProvider:notifications.getIDP.genericError.description"), level: AlertLevels.ERROR, message: t("authenticationProvider:notifications.getIDP.genericError.message") }) diff --git a/features/admin.connections.v1/pages/connection-test.tsx b/features/admin.connections.v1/pages/connection-test.tsx index 8e3b6c22409..9cf168f8afe 100644 --- a/features/admin.connections.v1/pages/connection-test.tsx +++ b/features/admin.connections.v1/pages/connection-test.tsx @@ -32,23 +32,61 @@ import Tab from "@oxygen-ui/react/Tab"; import TabPanel from "@oxygen-ui/react/TabPanel"; import Tabs from "@oxygen-ui/react/Tabs"; import Typography from "@oxygen-ui/react/Typography"; +import { Theme, useTheme } from "@mui/material/styles"; +import { AppConstants } from "@wso2is/admin.core.v1/constants/app-constants"; import { history } from "@wso2is/admin.core.v1/helpers/history"; import useResourceEndpoints from "@wso2is/admin.core.v1/hooks/use-resource-endpoints"; import { TabPageLayout } from "@wso2is/react-components"; -import React, { ReactElement, useEffect, useRef, useState } from "react"; +import { AxiosResponse } from "axios"; +import React, { FunctionComponent, ReactElement, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { RouteComponentProps } from "react-router-dom"; +import { IdentifiableComponentInterface } from "@wso2is/core/models"; import { Accordion, Icon } from "semantic-ui-react"; +import { + ConnectionTestSessionResponseInterface, + getConnectionTestResult, + resolveConnectionTestErrorMessage, + startConnectionTestSession +} from "../api/connection-test-api"; +import { + StyledCodeBlock, + StyledCodeBlockContainer, + StyledDiagnosticAccordionRow, + StyledDiagnosticAccordionTitle, + StyledDiagnosticsScroller, + StyledDiagnosticValueBlock, + StyledExpandedDetailsTable, + StyledExpandedLabelCell, + StyledExpandedValueCell, + StyledHeaderCell, + StyledMonoCell, + StyledResultCard, + StyledTableHeaderRow, + StyledTableRow, + StyledTableWrapper, + StyledValueCell +} from "../components/connection-test-styles"; +import { MappedClaimInterface } from "../models/connection"; +import { + ConnectionTestDiagnosticLogInterface, + ConnectionTestResultInterface, + ConnectionTestResultMetadataInterface, + ConnectionTestStepStatusInterface +} from "../models/connection-test"; - -/** - * Interface for the route parameters. - */ interface RouteParams { tenantDomain?: string; id?: string; } +interface ConnectionTestLocationStateInterface { + debugId?: string; +} + +interface ConnectionTestPagePropsInterface + extends IdentifiableComponentInterface, RouteComponentProps {} + const RotateIcon = (): ReactElement => { /* eslint-disable-next-line max-len */ return ; @@ -60,14 +98,18 @@ const RotateIcon = (): ReactElement => { * @param props - Props injected to the component. * @returns React element. */ -const ConnectionTestPage: React.FC> = (props) => { +const ConnectionTestPage: FunctionComponent = ( + props: ConnectionTestPagePropsInterface +): ReactElement => { + const theme: Theme = useTheme(); const { id: connectorId = "" } = props.match.params || {}; const { location } = props; - const resultCacheKey = `idp-test-result:${connectorId}`; - const locationState = location?.state as any; - const hasPendingAutoRun = Boolean(locationState?.debugId && locationState?.authorizationUrl); + const resultCacheKey: string = `idp-test-result:${connectorId}`; + const locationState: ConnectionTestLocationStateInterface | undefined = + location?.state as ConnectionTestLocationStateInterface | undefined; + const hasPendingAutoRun: boolean = Boolean(locationState?.debugId); - const getCachedResult = (): any => { + const getCachedResult = (): ConnectionTestResultInterface | null => { if (typeof window === "undefined" || !connectorId) { return null; } @@ -91,18 +133,20 @@ const ConnectionTestPage: React.FC> = (props) = const { resourceEndpoints } = useResourceEndpoints(); const [ debugId, setDebugId ] = useState(null); - const [ result, setResult ] = useState(() => hasPendingAutoRun ? null : getCachedResult()); + const [ result, setResult ] = useState( + () => hasPendingAutoRun ? null : getCachedResult() + ); const [ error, setError ] = useState(null); - const [ loading, setLoading ] = useState(false); - const [ hasError, setHasError ] = useState(false); - const [ hasPartial, setHasPartial ] = useState(false); - const [ activeTab, setActiveTab ] = useState(0); - const [ autoRunTriggered, setAutoRunTriggered ] = useState(false); - const [ isStatusBannerVisible, setIsStatusBannerVisible ] = useState(true); + const [ loading, setLoading ] = useState(false); + const [ hasError, setHasError ] = useState(false); + const [ hasPartial, setHasPartial ] = useState(false); + const [ activeTab, setActiveTab ] = useState(0); + const [ autoRunTriggered, setAutoRunTriggered ] = useState(false); + const [ isStatusBannerVisible, setIsStatusBannerVisible ] = useState(true); const [ expandedDiagnosticLogs, setExpandedDiagnosticLogs ] = useState([]); - const popupInterval = useRef(null); - const fetchTimer = useRef(null); + const popupInterval = useRef(null); + const fetchTimer = useRef(null); /** * Clears cached test result for the current connection. @@ -118,7 +162,7 @@ const ConnectionTestPage: React.FC> = (props) = /** * Saves latest test result payload to browser cache. */ - const cacheResult = (value: any): void => { + const cacheResult = (value: ConnectionTestResultInterface): void => { if (typeof window === "undefined" || !connectorId) { return; } @@ -148,12 +192,12 @@ const ConnectionTestPage: React.FC> = (props) = }, []); /** - * Auto-run test if debugId and authUrl are provided via location state. + * Auto-run result retrieval if a debug id is provided via location state. */ useEffect(() => { - const state = locationState; + const state: ConnectionTestLocationStateInterface | undefined = locationState; - if (state?.debugId && state?.authorizationUrl && !autoRunTriggered) { + if (state?.debugId && !autoRunTriggered) { // Reset stale UI/cached data before starting an auto-run flow. setResult(null); setError(null); @@ -166,25 +210,7 @@ const ConnectionTestPage: React.FC> = (props) = setAutoRunTriggered(true); setDebugId(state.debugId); - // Open the authorization URL - const authPopup = window.open(state.authorizationUrl, "_blank"); - - // Monitor the popup - popupInterval.current = setInterval(() => { - try { - if (authPopup && authPopup.closed) { - clearTimers(); - // Fetch results after popup closes - fetchTimer.current = setTimeout(() => { - fetchResult(state.debugId); - }, 1000); - } - } catch (e) { - // ignore - } - }, 500); - - // Set a timeout to fetch results after 30 seconds + // Fetch results after the authorization flow has had time to complete. fetchTimer.current = setTimeout(() => { clearTimers(); fetchResult(state.debugId); @@ -198,7 +224,7 @@ const ConnectionTestPage: React.FC> = (props) = */ const fetchResult = async (sid: string): Promise => { if (!sid) { - setError("No debug session id found..."); + setError(t("authenticationProvider:notifications.getIDP.genericError.description")); return; } @@ -209,11 +235,8 @@ const ConnectionTestPage: React.FC> = (props) = setExpandedDiagnosticLogs([]); try { - const axios = (await import("axios")).default; - const response = await axios.get( - `${resourceEndpoints.debug}/${sid}/result`, - { withCredentials: true } - ); + const response: AxiosResponse = + await getConnectionTestResult(resourceEndpoints, sid); clearCachedResult(); setResult(response.data); @@ -221,15 +244,19 @@ const ConnectionTestPage: React.FC> = (props) = cacheResult(response.data); setExpandedDiagnosticLogs([]); - } catch (err: any) { + } catch (error: unknown) { // eslint-disable-next-line no-console - console.error("[ConnectionTest] Fetch error:", err?.response?.status, err?.message); + console.error("[ConnectionTest] Fetch error:", resolveConnectionTestErrorMessage(error)); - if (err?.response?.status === 404) { - setError("Unable to retrieve test results for this session."); - } else { - setError(err?.response?.data?.message || err?.message || "Failed to fetch debug results."); + const errorMessage: string | undefined = resolveConnectionTestErrorMessage(error); + + if (errorMessage) { + setError(errorMessage); + + return; } + + setError(t("authenticationProvider:notifications.getIDP.genericError.description")); } finally { setLoading(false); } @@ -249,25 +276,24 @@ const ConnectionTestPage: React.FC> = (props) = clearCachedResult(); if (!connectorId) { - setError("Connection ID is missing. Cannot run test."); + setError(t("authenticationProvider:notifications.getIDP.genericError.description")); return; } try { - const axios = (await import("axios")).default; - const payload = { - connectionId: connectorId - }; + const response: AxiosResponse = + await startConnectionTestSession(resourceEndpoints, connectorId); - const response = await axios.post( - `${resourceEndpoints.debug}/idp`, - payload, - { withCredentials: true } - ); + const authorizationUrl: string | undefined = response.data?.metadata?.authorizationUrl; + const newDebugId: string | undefined = response.data?.debugId; - const newDebugId = response.data.debugId; - const authorizationUrl = response.data.metadata.authorizationUrl; + if (!newDebugId) { + clearTimers(); + setError(t("authenticationProvider:notifications.getIDP.genericError.description")); + + return; + } setDebugId(newDebugId); @@ -304,11 +330,14 @@ const ConnectionTestPage: React.FC> = (props) = fetchResult(newDebugId); }, 2000); } - } catch (error: any) { + } catch (error: unknown) { // eslint-disable-next-line no-console console.error("[ConnectionTest] Error running test:", error); clearTimers(); - setError(error?.response?.data?.message || error?.message || "Failed to run test."); + setError( + resolveConnectionTestErrorMessage(error) + ?? t("authenticationProvider:notifications.getIDP.genericError.description") + ); } }; @@ -366,13 +395,7 @@ const ConnectionTestPage: React.FC> = (props) = * Handles the back button click event. */ const handleBackButtonClick = (): void => { - // Extract tenant domain from current path - const pathParts = location.pathname.split("/"); - const tenantIndex = pathParts.indexOf("t"); - const tenantDomain = tenantIndex !== -1 ? pathParts[tenantIndex + 1] : "carbon.super"; - - // Navigate to the specific connection page - history.push(`/t/${tenantDomain}/console/connections/${connectorId}`); + history.push(AppConstants.getPaths().get("IDP_EDIT").replace(":id", connectorId)); }; // State for connector details - removed as we no longer display connector info on this page @@ -390,30 +413,17 @@ const ConnectionTestPage: React.FC> = (props) = wordBreak?: "break-all" | "break-word"; } = {} ) => ( - - + { typeof value === "string" ? value : JSON.stringify(value, null, 2) } - - + + ); const tabPanes = [ @@ -455,7 +465,7 @@ const ConnectionTestPage: React.FC> = (props) = { decoded ? ( - + Header @@ -470,7 +480,7 @@ const ConnectionTestPage: React.FC> = (props) = Signature { renderCodeBlock(decoded.signature, { wordBreak: "break-all" }) } - + ) : ( Unable to decode token or not a valid JWT. @@ -485,8 +495,13 @@ const ConnectionTestPage: React.FC> = (props) = key: "claim-mappings", menuItem: "Claim Mappings", render: () => { - const claimsArray = Array.isArray(result?.metadata?.mappedClaims) ? result?.metadata?.mappedClaims : []; - const sortedClaims = claimsArray.sort((a, b) => { + const claimsArray: MappedClaimInterface[] = Array.isArray(result?.metadata?.mappedClaims) + ? result?.metadata?.mappedClaims as MappedClaimInterface[] + : []; + const sortedClaims: MappedClaimInterface[] = [ ...claimsArray ].sort(( + a: MappedClaimInterface, + b: MappedClaimInterface + ) => { if (a.status === "Successful" && b.status !== "Successful") { return -1; } @@ -511,62 +526,43 @@ const ConnectionTestPage: React.FC> = (props) = return ( - + - - + Local Claim (URI) - - + IDP Claim - - + Value - - + + { sortedClaims.length === 0 && ( @@ -577,64 +573,49 @@ const ConnectionTestPage: React.FC> = (props) = ) } { sortedClaims.map((claim, idx) => ( - - { claim.localClaim || claim.isClaim || "-" } - - + { claim.idpClaim } - - + { formatValue(claim.value) } - - + + )) } + ); @@ -645,11 +626,14 @@ const ConnectionTestPage: React.FC> = (props) = menuItem: "Diagnosis", render: () => { const formatLogs = () => { - const metadataObj = result?.metadata || {}; - const stepStatus = metadataObj?.stepStatus || {}; - const allDiagnostics = Array.isArray(metadataObj?.diagnostics) ? metadataObj.diagnostics : []; + const metadataObj: ConnectionTestResultMetadataInterface = result?.metadata || {}; + const stepStatus: ConnectionTestStepStatusInterface = metadataObj?.stepStatus || {}; + const allDiagnostics: ConnectionTestDiagnosticLogInterface[] = + Array.isArray(metadataObj?.diagnostics) ? metadataObj.diagnostics : []; // Filter out 'started' status entries and claim validation stage entries - const diagnostics = allDiagnostics.filter((log: any) => + const diagnostics: ConnectionTestDiagnosticLogInterface[] = allDiagnostics.filter(( + log: ConnectionTestDiagnosticLogInterface + ) => !(log.status === "started" || log.stage === "claimValidation") ); const steps: Record = { @@ -669,21 +653,21 @@ const ConnectionTestPage: React.FC> = (props) = switch (normalizedStatus) { case "success": - return { background: "#E8F5E9", color: "#15803D" }; + return { background: theme.palette.success.light, color: theme.palette.success.dark }; case "partial": - return { background: "#FFFBEB", color: "#B45309" }; + return { background: theme.palette.warning.light, color: theme.palette.warning.dark }; case "started": case "pending": - return { background: "#E0F2FE", color: "#0284C7" }; + return { background: theme.palette.info.light, color: theme.palette.info.dark }; case "failed": case "error": - return { background: "#FEE4E2", color: "#B42318" }; + return { background: theme.palette.error.light, color: theme.palette.error.dark }; default: - return { background: "#F3F4F6", color: "#6B7280" }; + return { background: theme.palette.action.hover, color: theme.palette.text.secondary }; } }; const getDiagnosticStatusIcon = (status?: string): ReactElement => { - let iconName: string; + let iconName: "check circle" | "times circle"; let iconColor: "green" | "yellow" | "red"; switch (String(status || "").toUpperCase()) { @@ -744,29 +728,17 @@ const ConnectionTestPage: React.FC> = (props) = if (typeof value === "object") { return ( - { JSON.stringify(value, null, 2) } - + ); } return String(value); }; - const renderExpandedDiagnosticRows = (log: any) => { + const renderExpandedDiagnosticRows = (log: ConnectionTestDiagnosticLogInterface) => { const rows: Array<{ label: string; value: unknown }> = [ { label: "recordedAt", @@ -794,26 +766,7 @@ const ConnectionTestPage: React.FC> = (props) = { diagnostics.length > 0 && ( - + { diagnostics.map((log: any, idx: number) => { const timestamp = log.timestamp @@ -822,13 +775,9 @@ const ConnectionTestPage: React.FC> = (props) = const isExpanded = expandedDiagnosticLogs.includes(idx); return ( - > = (props) = onClick={ () => toggleDiagnosticLog(idx) } style={ { padding: 0 } } > - + > = (props) = color: "text.primary", flexShrink: 0, fontSize: 12, - marginLeft: "auto", + ml: "auto", textAlign: "right", whiteSpace: "nowrap" } } > { timestamp } - + - { renderExpandedDiagnosticRows(log).map((row) => ( - { row.label }: - - + { renderDiagnosticLogValue(row.value) } - + )) } - + - + ); }) } - + ) } diff --git a/identity-apps-core/apps/authentication-portal/src/main/webapp/debugError.jsp b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugError.jsp index 38b5de28938..0ab352cd351 100644 --- a/identity-apps-core/apps/authentication-portal/src/main/webapp/debugError.jsp +++ b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugError.jsp @@ -1,10 +1,22 @@ <%-- - ~ Copyright (c) 2025, WSO2 LLC. - ~ Licensed under the Apache License, Version 2.0. - ~ See the License for the specific language governing permissions and limitations under the License. + ~ Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + ~ + ~ WSO2 LLC. licenses this file to you under the Apache License, + ~ Version 2.0 (the "License"); you may not use this file except + ~ in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. --%> -<%@ page import="org.apache.commons.text.StringEscapeUtils" %> +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> <%@include file="includes/localize.jsp" %> @@ -49,7 +61,7 @@
@@ -88,14 +100,14 @@ const urlParams = new URLSearchParams(window.location.search); const idpId = urlParams.get('idpId'); const baseUrl = (function() { - var consoleUrlAttr = '<%= request.getAttribute("consoleUrl") != null ? request.getAttribute("consoleUrl") : "" %>'; + var consoleUrlAttr = '<%= request.getAttribute("consoleUrl") != null ? Encode.forJavaScript(String.valueOf(request.getAttribute("consoleUrl"))) : "" %>'; if (consoleUrlAttr && consoleUrlAttr !== "") return consoleUrlAttr; - var tenanted = '<%= request.getAttribute("tenantedConsoleUrl") != null ? request.getAttribute("tenantedConsoleUrl") : "" %>'; + var tenanted = '<%= request.getAttribute("tenantedConsoleUrl") != null ? Encode.forJavaScript(String.valueOf(request.getAttribute("tenantedConsoleUrl"))) : "" %>'; if (tenanted && tenanted !== "") return tenanted; - var serverUrl = '<%= request.getAttribute("serverUrl") != null ? request.getAttribute("serverUrl") : "" %>'; - var tenantPrefix = '<%= request.getAttribute("tenantPrefix") != null ? request.getAttribute("tenantPrefix") : "" %>'; + var serverUrl = '<%= request.getAttribute("serverUrl") != null ? Encode.forJavaScript(String.valueOf(request.getAttribute("serverUrl"))) : "" %>'; + var tenantPrefix = '<%= request.getAttribute("tenantPrefix") != null ? Encode.forJavaScript(String.valueOf(request.getAttribute("tenantPrefix"))) : "" %>'; if (serverUrl && tenantPrefix) return serverUrl + tenantPrefix; return (window.location && window.location.origin @@ -107,16 +119,14 @@ var connectionUrl = baseUrl + "/console/connections/" + encodeURIComponent(idpId || ""); // Back button redirect - document.getElementById("backBtn").onclick = function() { - window.location.href = connectionUrl; - }; - - // Close popup if applicable - window.close(); - + var backBtn = document.getElementById("backBtn"); + if (backBtn) { + backBtn.onclick = function () { + window.location.href = connectionUrl; + }; + } } catch (e) { - console.error("Failed to parse or display JSON result:", e); - document.getElementById("json").textContent = "Error displaying JSON. See browser console for details."; + console.error("Failed to build connection URL for back button:", e); } diff --git a/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp index 59991578ead..135967d5fe9 100644 --- a/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp +++ b/identity-apps-core/apps/authentication-portal/src/main/webapp/debugSuccess.jsp @@ -1,121 +1,3 @@ -<%-- - ~ Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). - ~ - ~ WSO2 LLC. licenses this file to you under the Apache License, - ~ Version 2.0 (the "License"); you may not use this file except - ~ in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, - ~ software distributed under the License is distributed on an - ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - ~ KIND, either express or implied. See the License for the - ~ specific language governing permissions and limitations - ~ under the License. ---%> - -<%@ page import="java.io.File" %> -<%@ page import="org.wso2.carbon.identity.application.authentication.endpoint.util.AuthenticationEndpointUtil" %> -<%@ page import="org.owasp.encoder.Encode" %> -<%@ page import="org.apache.commons.text.StringEscapeUtils" %> -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> -<%@ taglib prefix="layout" uri="org.wso2.identity.apps.taglibs.layout.controller" %> -<%@ include file="includes/localize.jsp" %> - -<%-- Include tenant context --%> - - -<%-- Branding Preferences --%> - - -<% request.setAttribute("pageName","debug-success"); %> - -<%-- Data for the layout from the page --%> -<% - layoutData.put("isResponsePage", true); - layoutData.put("isSuccessResponse", true); - layoutData.put("isDebugSuccessPage", true); -%> - - - - - <%-- header --%> - <% - File headerFile = new File(getServletContext().getRealPath("extensions/header.jsp")); - if (headerFile.exists()) { - %> - - <% } else { %> - - <% } %> - -"> - - - <%-- product-title --%> - <% - File productTitleFile = new File(getServletContext().getRealPath("extensions/product-title.jsp")); - if (productTitleFile.exists()) { - %> - - <% } else { %> - - <% } %> - - -
-
-
-

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "Authentication Successful")%>

-
-
- <%=AuthenticationEndpointUtil.i18n(resourceBundle, "You will be redirected shortly.")%> -
-
-
-
- - <%-- product-footer --%> - <% - File productFooterFile = new File(getServletContext().getRealPath("extensions/product-footer.jsp")); - if (productFooterFile.exists()) { - %> - - <% } else { %> - - <% } %> - - - - -
- - <%-- footer --%> - <% - File footerFile = new File(getServletContext().getRealPath("extensions/footer.jsp")); - if (footerFile.exists()) { - %> - - <% } else { %> - - <% } %> - - - - <%-- ~ Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). ~ @@ -200,8 +82,10 @@

<%=AuthenticationEndpointUtil.i18n(resourceBundle, "Debug Request was Completed")%>

- <%= AuthenticationEndpointUtil.i18n(resourceBundle, "You will be redirected to results page in ") %> - 2 s + <%= AuthenticationEndpointUtil.i18n( + resourceBundle, + "You will be redirected to results page in {0} s" + ).replace("{0}", "2") %>
@@ -235,8 +119,9 @@