From 6eaecdd0f8af577a47bc33def6406adfef7f08f7 Mon Sep 17 00:00:00 2001 From: GanJiaKouN16 Date: Sat, 6 Jun 2026 14:15:42 +0800 Subject: [PATCH 1/2] feat: add reset password functionality to workspace UI Wire the existing GenerateResetLinkModal and PasswordResetLinkModal into the Actions dropdown in the workspace members table. - Add 'Reset password' menu item for workspace members (not self) - Add resetPassword API function in profile service - Show confirmation dialog before generating the reset link - Display the generated password reset link with copy functionality Closes #2572 --- .../WorkspaceManage/cellRenderers.tsx | 57 ++++++++++++++++++- web/oss/src/services/profile/index.ts | 14 +++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx b/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx index d689d57135..7be7d3befd 100644 --- a/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx +++ b/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx @@ -3,7 +3,7 @@ import {useState} from "react" import type {User} from "@agenta/shared/types" import {message} from "@agenta/ui/app-message" import {EditOutlined, MoreOutlined, SyncOutlined} from "@ant-design/icons" -import {ArrowClockwise, Trash} from "@phosphor-icons/react" +import {ArrowClockwise, Key, Trash} from "@phosphor-icons/react" import {Button, Dropdown, Input, Modal, Space, Tag, Tooltip, Typography} from "antd" import AlertPopup from "@/oss/components/AlertPopup/AlertPopup" @@ -12,7 +12,7 @@ import {isEmailInvitationsEnabled} from "@/oss/lib/helpers/isEE" import {useEntitlements} from "@/oss/lib/helpers/useEntitlements" import {snakeToTitle} from "@/oss/lib/helpers/utils" import {WorkspaceMember} from "@/oss/lib/Types" -import {updateUsername} from "@/oss/services/profile" +import {resetPassword, updateUsername} from "@/oss/services/profile" import { assignWorkspaceRole, removeFromWorkspace, @@ -23,6 +23,9 @@ import {useOrgData} from "@/oss/state/org" import {useProfileData} from "@/oss/state/profile" import {useWorkspaceRoles} from "@/oss/state/workspace" +import GenerateResetLinkModal from "./Modals/GenerateResetLinkModal" +import PasswordResetLinkModal from "./Modals/PasswordResetLinkModal" + export const Actions: React.FC<{ member: WorkspaceMember hidden?: boolean @@ -39,6 +42,10 @@ export const Actions: React.FC<{ const {refetch: refetchProfile} = useProfileData() const [renameOpen, setRenameOpen] = useState(false) const [renameValue, setRenameValue] = useState(user.username || "") + const [generateResetLinkOpen, setGenerateResetLinkOpen] = useState(false) + const [resetLinkOpen, setResetLinkOpen] = useState(false) + const [resetLink, setResetLink] = useState("") + const [resetLoading, setResetLoading] = useState(false) if (hidden && !selfMenu) return null @@ -90,6 +97,24 @@ export const Actions: React.FC<{ } } + const handleResetPassword = async () => { + setResetLoading(true) + try { + const link = await resetPassword(user.id) + setGenerateResetLinkOpen(false) + setResetLink(link) + setResetLinkOpen(true) + } catch (error: any) { + const detail = + error?.response?.data?.detail || + error?.message || + "Unable to generate reset password link" + message.error(detail) + } finally { + setResetLoading(false) + } + } + return ( <> , + onClick: (e: any) => { + e.domEvent.stopPropagation() + setGenerateResetLinkOpen(true) + }, + }, + ] + : []), { key: "remove", label: "Remove", @@ -165,6 +203,21 @@ export const Actions: React.FC<{ placeholder="New username" /> + + setGenerateResetLinkOpen(false)} + onOk={handleResetPassword} + confirmLoading={resetLoading} + /> + + setResetLinkOpen(false)} + /> ) } diff --git a/web/oss/src/services/profile/index.ts b/web/oss/src/services/profile/index.ts index 0b3816c710..0acfcedb48 100644 --- a/web/oss/src/services/profile/index.ts +++ b/web/oss/src/services/profile/index.ts @@ -56,3 +56,17 @@ export const changePassword = async (payload: { body: JSON.stringify(payload), }) } + +/** + * Generate a password reset link for a user (admin action). + * Returns the reset password link string. + */ +export const resetPassword = async (userId: string): Promise => { + const base = getBaseUrl() + const url = new URL("api/profile/reset-password", base) + url.searchParams.set("user_id", userId) + const data = await fetchJson(url, { + method: "POST", + }) + return data +} From 445fc3f700e0ac46c7c4e3fbb653039a56d8bbe6 Mon Sep 17 00:00:00 2001 From: GanJiaKouN16 Date: Sat, 6 Jun 2026 14:36:28 +0800 Subject: [PATCH 2/2] fix: add shouldIgnoreRowClick guard to unprotected tables Several tables with row-level click navigation were missing the shouldIgnoreRowClick guard, causing clicks on interactive elements (checkboxes, dropdowns, buttons) to accidentally trigger row navigation. Changes: - Consolidate shouldIgnoreRowClick with broader selector list (merges EvaluationRunsTablePOC's extra selectors: [role='button'], [role='menuitem'], [role='checkbox'], .ant-btn, etc.) - Export INTERACTIVE_ROW_SELECTORS constant for reuse - Add guard to ObservabilityTable (traces) - Add guard to SessionsTable - Add guard to PromptsPage - Add guard to TestcasesTableShell - Add guard to EntityTable - Replace partial data-ivt-stop-row-click check in ScenarioListView with full shouldIgnoreRowClick - Update useEntityTableState to use consolidated selectors - Remove duplicate shouldIgnoreRowClick from navigationActions.ts - Update EvaluationRunsTablePOC to import from shared utility Closes #3254 --- .../actions/navigationActions.ts | 10 ----- .../components/EvaluationRunsTable/index.tsx | 2 +- .../hooks/useTableManager.tsx | 42 ++++++++++++------- .../components/TestcasesTableShell.tsx | 8 +++- .../components/ObservabilityTable/index.tsx | 7 +++- .../components/SessionsTable/index.tsx | 7 +++- .../components/pages/prompts/PromptsPage.tsx | 6 ++- .../AnnotationSession/ScenarioListView.tsx | 4 +- .../src/shared/EntityTable.tsx | 7 +++- .../hooks/useEntityTableState.ts | 17 +++----- .../hooks/useTableManager.tsx | 42 ++++++++++++------- 11 files changed, 86 insertions(+), 66 deletions(-) diff --git a/web/oss/src/components/EvaluationRunsTablePOC/actions/navigationActions.ts b/web/oss/src/components/EvaluationRunsTablePOC/actions/navigationActions.ts index bf4160796b..8f1ac7dc66 100644 --- a/web/oss/src/components/EvaluationRunsTablePOC/actions/navigationActions.ts +++ b/web/oss/src/components/EvaluationRunsTablePOC/actions/navigationActions.ts @@ -1,5 +1,3 @@ -import type {MouseEvent} from "react" - import {message} from "@agenta/ui/app-message" import {getDefaultStore} from "jotai" import Router from "next/router" @@ -23,14 +21,6 @@ const getUrlState = (): URLState => store.get(urlAtom) as URLState const getActiveAppId = (): string | null => store.get(routerAppIdAtom) -export const shouldIgnoreRowClick = (event: MouseEvent) => { - const target = event.target as HTMLElement | null - if (!target) return false - const interactiveSelector = - "button, a, input, textarea, select, [role='button'], [role='menuitem'], [role='checkbox'], .ant-checkbox, .ant-checkbox-input, .ant-checkbox-inner, .ant-checkbox-wrapper, .ant-btn, .ant-select, .ant-dropdown-trigger" - return Boolean(target.closest(interactiveSelector)) -} - interface NavigateToRunParams { record: EvaluationRunTableRow scope: "app" | "project" diff --git a/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/index.tsx b/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/index.tsx index 381c42a3c5..e4aa5e1fd3 100644 --- a/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/index.tsx +++ b/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/index.tsx @@ -13,6 +13,7 @@ import {activePreviewProjectIdAtom} from "@/oss/components/EvalRunDetails/atoms/ import {clearAllMetricStatsCaches} from "@/oss/components/EvalRunDetails/atoms/runMetrics" import { InfiniteVirtualTableFeatureShell, + shouldIgnoreRowClick, type TableFeaturePagination, type TableScopeConfig, } from "@/oss/components/InfiniteVirtualTable" @@ -34,7 +35,6 @@ import { } from "@/oss/lib/onboarding" import {useQueryParamState} from "@/oss/state/appState" -import {shouldIgnoreRowClick} from "../../actions/navigationActions" import { evaluationRunsDeleteContextAtom, evaluationRunsTableFetchEnabledAtom, diff --git a/web/oss/src/components/InfiniteVirtualTable/hooks/useTableManager.tsx b/web/oss/src/components/InfiniteVirtualTable/hooks/useTableManager.tsx index be69f2bad5..8c38dce80f 100644 --- a/web/oss/src/components/InfiniteVirtualTable/hooks/useTableManager.tsx +++ b/web/oss/src/components/InfiniteVirtualTable/hooks/useTableManager.tsx @@ -27,26 +27,36 @@ import useTableExport from "./useTableExport" const dummySearchAtom = atom("") /** - * Helper to detect if a click event should be ignored for row navigation + * Default CSS selectors for interactive elements that should not trigger row navigation. + * Consolidated from all table implementations to ensure consistent click-through behavior. + */ +export const INTERACTIVE_ROW_SELECTORS = [ + "button", + "a", + "input", + "textarea", + "select", + "[role='button']", + "[role='menuitem']", + "[role='checkbox']", + "[data-interactive]", + ".ant-dropdown-trigger", + ".ant-checkbox-wrapper", + ".ant-checkbox", + ".ant-checkbox-input", + ".ant-checkbox-inner", + ".ant-btn", + ".ant-select", +].join(", ") + +/** + * Helper to detect if a click event should be ignored for row navigation. * Returns true if the click was on an interactive element (button, link, dropdown, etc.) */ export const shouldIgnoreRowClick = (event: MouseEvent): boolean => { const target = event.target as HTMLElement - - // Check if clicking on interactive elements - if ( - target.closest("button") || - target.closest("a") || - target.closest(".ant-dropdown-trigger") || - target.closest(".ant-checkbox-wrapper") || - target.closest(".ant-select") || - target.closest("input") || - target.closest("textarea") - ) { - return true - } - - return false + if (!target) return false + return Boolean(target.closest(INTERACTIVE_ROW_SELECTORS)) } /** Configuration for built-in search. When provided, the hook manages search state internally. */ diff --git a/web/oss/src/components/TestcasesTableNew/components/TestcasesTableShell.tsx b/web/oss/src/components/TestcasesTableNew/components/TestcasesTableShell.tsx index a046d6ec5c..a9663cd8d5 100644 --- a/web/oss/src/components/TestcasesTableNew/components/TestcasesTableShell.tsx +++ b/web/oss/src/components/TestcasesTableNew/components/TestcasesTableShell.tsx @@ -1,10 +1,11 @@ -import {useCallback, useMemo, useState} from "react" +import React, {useCallback, useMemo, useState} from "react" import { ColumnVisibilityMenuTrigger, defaultHeaderVariant, detectColumnTypes, InfiniteVirtualTableFeatureShell, + shouldIgnoreRowClick, type TableScopeConfig, type TypeChipConfig, useTypeChipFeature, @@ -758,7 +759,10 @@ export function TestcasesTableShell(props: TestcasesTableShellProps) { size: "small" as const, bordered: true, onRow: (record: TestcaseTableRow) => ({ - onClick: () => onRowClick(record), + onClick: (event: React.MouseEvent) => { + if (shouldIgnoreRowClick(event)) return + onRowClick(record) + }, className: "cursor-pointer hover:bg-gray-50", }), }), diff --git a/web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx b/web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx index d6a94663ae..9bf9056264 100644 --- a/web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx +++ b/web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx @@ -1,6 +1,6 @@ import {type Key, type ReactNode, useCallback, useEffect, useMemo, useState} from "react" -import {InfiniteVirtualTableFeatureShell} from "@agenta/ui/table" +import {InfiniteVirtualTableFeatureShell, shouldIgnoreRowClick} from "@agenta/ui/table" import type {TableFeaturePagination, TableScopeConfig} from "@agenta/ui/table" import {useAtomValue, useSetAtom, useStore} from "jotai" import dynamic from "next/dynamic" @@ -307,7 +307,10 @@ const ObservabilityTable = () => { sticky: true, style: {cursor: "pointer"}, onRow: (record, index) => ({ - onClick: () => handleTraceRowClick(record), + onClick: (event) => { + if (shouldIgnoreRowClick(event)) return + handleTraceRowClick(record) + }, "data-tour": index === 0 ? "trace-row" : undefined, }), }} diff --git a/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx b/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx index 0d15650646..66282cbf3a 100644 --- a/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx +++ b/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx @@ -1,6 +1,6 @@ import {useCallback, useEffect, useMemo, useState} from "react" -import {InfiniteVirtualTableFeatureShell} from "@agenta/ui/table" +import {InfiniteVirtualTableFeatureShell, shouldIgnoreRowClick} from "@agenta/ui/table" import type {TableFeaturePagination, TableScopeConfig} from "@agenta/ui/table" import {useAtomValue, useSetAtom} from "jotai" import dynamic from "next/dynamic" @@ -141,7 +141,10 @@ const SessionsTable: React.FC = () => { bordered: true, loading: isLoading && sessionIds.length === 0, onRow: (record) => ({ - onClick: () => openDrawer({sessionId: record.session_id}), + onClick: (event) => { + if (shouldIgnoreRowClick(event)) return + openDrawer({sessionId: record.session_id}) + }, style: {cursor: "pointer"}, }), }} diff --git a/web/oss/src/components/pages/prompts/PromptsPage.tsx b/web/oss/src/components/pages/prompts/PromptsPage.tsx index f7c12e303c..ff959eb488 100644 --- a/web/oss/src/components/pages/prompts/PromptsPage.tsx +++ b/web/oss/src/components/pages/prompts/PromptsPage.tsx @@ -12,6 +12,7 @@ import type { TableFeaturePagination, TableScopeConfig, } from "@agenta/ui/table" +import {shouldIgnoreRowClick} from "@agenta/ui/table" import {message} from "antd" import type {TableProps} from "antd/es/table" import {useAtomValue, useSetAtom} from "jotai" @@ -686,7 +687,10 @@ const PromptsPage = () => { scroll: {x: "max-content" as const}, expandable: tableExpandableConfig, onRow: (record: PromptsTableRow) => ({ - onClick: () => handleRowClick(record), + onClick: (event: React.MouseEvent) => { + if (shouldIgnoreRowClick(event)) return + handleRowClick(record) + }, className: "cursor-pointer", draggable: true, onDragStart: (event: any) => { diff --git a/web/packages/agenta-annotation-ui/src/components/AnnotationSession/ScenarioListView.tsx b/web/packages/agenta-annotation-ui/src/components/AnnotationSession/ScenarioListView.tsx index 3dec2aebb6..1d33dde571 100644 --- a/web/packages/agenta-annotation-ui/src/components/AnnotationSession/ScenarioListView.tsx +++ b/web/packages/agenta-annotation-ui/src/components/AnnotationSession/ScenarioListView.tsx @@ -38,6 +38,7 @@ import { EXPORT_RESOLVE_SKIP, InfiniteVirtualTableFeatureShell, createActionsColumn, + shouldIgnoreRowClick, type InfiniteVirtualTableRowSelection, type TableScopeConfig, type TableExportColumnContext, @@ -1674,8 +1675,7 @@ const ScenarioListView = memo(function ScenarioListView({ // Row click opens annotation drawer const handleRowClick = useCallback((_event: React.MouseEvent, record: ScenarioTableRow) => { - const target = _event.target as HTMLElement - if (target?.closest("[data-ivt-stop-row-click]")) return + if (shouldIgnoreRowClick(_event)) return setDrawerScenarioId(record.scenarioId) }, []) diff --git a/web/packages/agenta-entity-ui/src/shared/EntityTable.tsx b/web/packages/agenta-entity-ui/src/shared/EntityTable.tsx index e92044c6cf..40066616b9 100644 --- a/web/packages/agenta-entity-ui/src/shared/EntityTable.tsx +++ b/web/packages/agenta-entity-ui/src/shared/EntityTable.tsx @@ -52,6 +52,7 @@ import {bgColors, cn} from "@agenta/ui/styles" import { buildEntityColumns, InfiniteVirtualTableFeatureShell, + shouldIgnoreRowClick, type BuildEntityColumnsOptions, type RowHeightFeatureConfig, type TableScopeConfig, @@ -546,8 +547,10 @@ export function EntityTable< bordered: true, onRow: selectable ? (record) => ({ - onClick: () => - handleRowSelect(record.id, !selectedIdsSet.has(record.id)), + onClick: (event) => { + if (shouldIgnoreRowClick(event)) return + handleRowSelect(record.id, !selectedIdsSet.has(record.id)) + }, className: cn( "cursor-pointer", selectedIdsSet.has(record.id) && bgColors.subtle, diff --git a/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useEntityTableState.ts b/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useEntityTableState.ts index 4ddfe1df5e..37ab19f0aa 100644 --- a/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useEntityTableState.ts +++ b/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useEntityTableState.ts @@ -72,6 +72,8 @@ import type { } from "../paginated/createPaginatedEntityStore" import type {InfiniteTableRowBase} from "../types" +import {INTERACTIVE_ROW_SELECTORS} from "./useTableManager" + // ============================================================================ // TYPES // ============================================================================ @@ -182,19 +184,10 @@ export interface UseEntityTableStateResult { // ============================================================================ /** - * Default selectors for interactive elements that should not trigger row click + * Default selectors for interactive elements that should not trigger row click. + * Uses the consolidated selector string from useTableManager for consistency. */ -const DEFAULT_INTERACTIVE_SELECTORS = [ - "button", - "a", - ".ant-dropdown-trigger", - ".ant-checkbox-wrapper", - ".ant-select", - "input", - "textarea", - "[role='button']", - "[data-interactive]", -] +const DEFAULT_INTERACTIVE_SELECTORS = INTERACTIVE_ROW_SELECTORS.split(", ") // ============================================================================ // HOOK diff --git a/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useTableManager.tsx b/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useTableManager.tsx index 82aafa7963..bc8dde1690 100644 --- a/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useTableManager.tsx +++ b/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useTableManager.tsx @@ -28,26 +28,36 @@ import useTableExport from "./useTableExport" const dummySearchAtom = atom("") /** - * Helper to detect if a click event should be ignored for row navigation + * Default CSS selectors for interactive elements that should not trigger row navigation. + * Consolidated from all table implementations to ensure consistent click-through behavior. + */ +export const INTERACTIVE_ROW_SELECTORS = [ + "button", + "a", + "input", + "textarea", + "select", + "[role='button']", + "[role='menuitem']", + "[role='checkbox']", + "[data-interactive]", + ".ant-dropdown-trigger", + ".ant-checkbox-wrapper", + ".ant-checkbox", + ".ant-checkbox-input", + ".ant-checkbox-inner", + ".ant-btn", + ".ant-select", +].join(", ") + +/** + * Helper to detect if a click event should be ignored for row navigation. * Returns true if the click was on an interactive element (button, link, dropdown, etc.) */ export const shouldIgnoreRowClick = (event: MouseEvent): boolean => { const target = event.target as HTMLElement - - // Check if clicking on interactive elements - if ( - target.closest("button") || - target.closest("a") || - target.closest(".ant-dropdown-trigger") || - target.closest(".ant-checkbox-wrapper") || - target.closest(".ant-select") || - target.closest("input") || - target.closest("textarea") - ) { - return true - } - - return false + if (!target) return false + return Boolean(target.closest(INTERACTIVE_ROW_SELECTORS)) } /** Configuration for built-in search. When provided, the hook manages search state internally. */