Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -23,14 +21,6 @@ const getUrlState = (): URLState => store.get(urlAtom) as URLState

const getActiveAppId = (): string | null => store.get(routerAppIdAtom)

export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>) => {
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -34,7 +35,6 @@ import {
} from "@/oss/lib/onboarding"
import {useQueryParamState} from "@/oss/state/appState"

import {shouldIgnoreRowClick} from "../../actions/navigationActions"
import {
evaluationRunsDeleteContextAtom,
evaluationRunsTableFetchEnabledAtom,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>): 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. */
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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",
}),
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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,
}),
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"},
}),
}}
Expand Down
6 changes: 5 additions & 1 deletion web/oss/src/components/pages/prompts/PromptsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -90,6 +97,24 @@ export const Actions: React.FC<{
}
}

const handleResetPassword = async () => {
setResetLoading(true)
try {
const link = await resetPassword(user.id)
setGenerateResetLinkOpen(false)
Comment on lines +100 to +104
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Route reset-password through the Fern SDK client, not the manual fetch path.

This handler now depends on a new API call path, but resetPassword is implemented with manual URL + fetchJson instead of the Fern client contract used in web/**/*.{ts,tsx}. Please migrate that service call to getAgentaSdkClient({host: getAgentaApiUrl()}) and pass params via {queryParams: {...}} before merge.

As per coding guidelines, all new frontend API code must go through the Fern-generated client and use {queryParams} rather than axios/fetch-style params.

Source: Coding guidelines

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 (
<>
<Dropdown
Expand Down Expand Up @@ -127,6 +152,19 @@ export const Actions: React.FC<{
},
]
: []),
...(isMember
? [
{
key: "reset_password",
label: "Reset password",
icon: <Key size={16} />,
onClick: (e: any) => {
e.domEvent.stopPropagation()
setGenerateResetLinkOpen(true)
},
},
]
: []),
{
key: "remove",
label: "Remove",
Expand Down Expand Up @@ -165,6 +203,21 @@ export const Actions: React.FC<{
placeholder="New username"
/>
</Modal>

<GenerateResetLinkModal
open={generateResetLinkOpen}
username={user.username}
onCancel={() => setGenerateResetLinkOpen(false)}
onOk={handleResetPassword}
confirmLoading={resetLoading}
/>

<PasswordResetLinkModal
open={resetLinkOpen}
username={user.username}
generatedLink={resetLink}
onCancel={() => setResetLinkOpen(false)}
/>
Comment on lines +207 to +220
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the modal components use EnhancedModal or raw antd Modal

fd -e tsx -e ts 'GenerateResetLinkModal|PasswordResetLinkModal' web/oss/src/components/pages/settings/WorkspaceManage/Modals/ --exec cat {}

Repository: Agenta-AI/agenta

Length of output: 4194


Switch WorkspaceManage reset modals to EnhancedModal

  • web/oss/src/components/pages/settings/WorkspaceManage/Modals/GenerateResetLinkModal.tsx imports and renders Modal from antd (should use EnhancedModal from @agenta/ui).
  • web/oss/src/components/pages/settings/WorkspaceManage/Modals/PasswordResetLinkModal.tsx imports and renders Modal from antd (should use EnhancedModal from @agenta/ui).

Source: Coding guidelines

Comment on lines +215 to +220
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear the generated reset link when closing the modal.

On Line 219, close only toggles resetLinkOpen; the reset URL remains in state. Since this is a sensitive token, clear it on close (and optionally before a new generation) to reduce in-memory retention.

Suggested patch
             <PasswordResetLinkModal
                 open={resetLinkOpen}
                 username={user.username}
                 generatedLink={resetLink}
-                onCancel={() => setResetLinkOpen(false)}
+                onCancel={() => {
+                    setResetLinkOpen(false)
+                    setResetLink("")
+                }}
             />

</>
)
}
Expand Down
14 changes: 14 additions & 0 deletions web/oss/src/services/profile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> => {
const base = getBaseUrl()
const url = new URL("api/profile/reset-password", base)
url.searchParams.set("user_id", userId)
const data = await fetchJson<string>(url, {
method: "POST",
})
return data
}
Comment on lines +60 to +72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the Fern-generated client exports a method for password reset

rg -n -C3 'resetUserPassword|reset.*password' --type=ts --iglob '**/agenta-api-client/**'

Repository: Agenta-AI/agenta

Length of output: 5484


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find ResetUserPasswordRequest shape and response handling
rg -n --type=ts 'ResetUserPasswordRequest' web/packages/agenta-api-client/src/generated/api -S

Repository: Agenta-AI/agenta

Length of output: 809


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the request interface
echo "---- ResetUserPasswordRequest.ts ----"
sed -n '1,120p' web/packages/agenta-api-client/src/generated/api/resources/users/client/requests/ResetUserPasswordRequest.ts

# Read the relevant portion of UsersClient.resetUserPassword
echo "---- Client.ts (resetUserPassword) ----"
sed -n '130,260p' web/packages/agenta-api-client/src/generated/api/resources/users/client/Client.ts

Repository: Agenta-AI/agenta

Length of output: 4067


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --type=ts --type=tsx '\.resetUserPassword\(' web/oss web/ee web -S

Repository: Agenta-AI/agenta

Length of output: 88


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n '\.resetUserPassword\(' -S --glob '**/*.ts' --glob '**/*.tsx' web/oss web/ee

Repository: Agenta-AI/agenta

Length of output: 42


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'resetUserPassword' -S --glob '**/*.ts' --glob '**/*.tsx' web/oss web/ee

Repository: Agenta-AI/agenta

Length of output: 42


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --glob '**/*.ts' --glob '**/*.tsx' 'getAgentaSdkClient\(\{host: getAgentaApiUrl\(\)\}\)' web/oss web/ee

Repository: Agenta-AI/agenta

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,120p' web/oss/src/components/pages/_app/index.tsx

Repository: Agenta-AI/agenta

Length of output: 3896


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'ResetUserPasswordResponse|resetUserPassword.*Response|reset password' web/packages/agenta-api-client/src/generated/api/resources/users/client -S

Repository: Agenta-AI/agenta

Length of output: 233


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'reset-password|ResetUserPassword' web/packages/agenta-api-client/src/generated/api -S

Repository: Agenta-AI/agenta

Length of output: 1139


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'profile/reset-password|reset_password|resetUserPassword' web/packages/agenta-api-client/src/generated/api -S

Repository: Agenta-AI/agenta

Length of output: 1066


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find how the SDK responses are consumed (e.g., `.data`)
rg -n --glob '**/*.ts' --glob '**/*.tsx' \
  '\.data(\b|$)|rawResponse|HttpResponsePromise' web/oss/src web/ee/src \
  || true

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find how the SDK client return values are consumed (e.g., awaiting and reading `.data`)
rg -n --glob '**/*.ts' --glob '**/*.tsx' 'getAgentaSdkClient\(' web/oss web/ee

Repository: Agenta-AI/agenta

Length of output: 153


Use the Fern-generated client for resetPassword

resetPassword manually builds the URL and uses fetchJson, but the Fern SDK already exposes client.users.resetUserPassword({ user_id }) for the POST profile/reset-password endpoint. Refactor to call that method via getAgentaSdkClient({ host: getAgentaApiUrl() }) and keep the return value consistent with the current Promise<string> contract.

Source: Coding guidelines

Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
EXPORT_RESOLVE_SKIP,
InfiniteVirtualTableFeatureShell,
createActionsColumn,
shouldIgnoreRowClick,
type InfiniteVirtualTableRowSelection,
type TableScopeConfig,
type TableExportColumnContext,
Expand Down Expand Up @@ -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)
}, [])

Expand Down
7 changes: 5 additions & 2 deletions web/packages/agenta-entity-ui/src/shared/EntityTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {bgColors, cn} from "@agenta/ui/styles"
import {
buildEntityColumns,
InfiniteVirtualTableFeatureShell,
shouldIgnoreRowClick,
type BuildEntityColumnsOptions,
type RowHeightFeatureConfig,
type TableScopeConfig,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ import type {
} from "../paginated/createPaginatedEntityStore"
import type {InfiniteTableRowBase} from "../types"

import {INTERACTIVE_ROW_SELECTORS} from "./useTableManager"

// ============================================================================
// TYPES
// ============================================================================
Expand Down Expand Up @@ -182,19 +184,10 @@ export interface UseEntityTableStateResult<TRow extends InfiniteTableRowBase> {
// ============================================================================

/**
* 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
Expand Down
Loading