mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
fix(web): three small UX fixes on /datasets and /plugins (#35514)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
8cb56713c0
commit
ddd546ef88
@ -3542,11 +3542,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-page/debug-info.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-page/empty/index.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 2
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Popover } from '@langgenius/dify-ui/popover'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@ -7,18 +8,15 @@ vi.mock('@/hooks/use-api-access-url', () => ({
|
||||
useDatasetApiAccessUrl: () => 'https://docs.dify.ai/api-reference/datasets',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/develop/secret-key/secret-key-modal', () => ({
|
||||
default: ({ isShow, onClose }: { isShow: boolean, onClose: () => void }) =>
|
||||
isShow ? <div data-testid="secret-key-modal"><button onClick={onClose}>close</button></div> : null,
|
||||
}))
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
})
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<Popover open>
|
||||
{children}
|
||||
</Popover>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
@ -28,8 +26,10 @@ const renderWithProviders = (ui: React.ReactElement) => {
|
||||
}
|
||||
|
||||
describe('Card (Service API)', () => {
|
||||
const onOpenSecretKeyModal = vi.fn()
|
||||
const defaultProps = {
|
||||
apiBaseUrl: 'https://api.dify.ai/v1',
|
||||
onOpenSecretKeyModal,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@ -77,48 +77,33 @@ describe('Card (Service API)', () => {
|
||||
// Props: tests different apiBaseUrl values
|
||||
describe('Props', () => {
|
||||
it('should display provided apiBaseUrl', () => {
|
||||
renderWithProviders(<Card apiBaseUrl="https://custom-api.example.com" />)
|
||||
renderWithProviders(<Card apiBaseUrl="https://custom-api.example.com" onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
expect(screen.getByText('https://custom-api.example.com')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show green indicator when apiBaseUrl is provided', () => {
|
||||
renderWithProviders(<Card apiBaseUrl="https://api.dify.ai" />)
|
||||
renderWithProviders(<Card apiBaseUrl="https://api.dify.ai" onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
// The Indicator component receives color="green" when apiBaseUrl is truthy
|
||||
const statusText = screen.getByText(/serviceApi\.enabled/)
|
||||
expect(statusText).toHaveClass('text-text-success')
|
||||
})
|
||||
|
||||
it('should show yellow indicator when apiBaseUrl is empty', () => {
|
||||
renderWithProviders(<Card apiBaseUrl="" />)
|
||||
renderWithProviders(<Card apiBaseUrl="" onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
// Still shows "enabled" text but indicator color differs
|
||||
expect(screen.getByText(/serviceApi\.enabled/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User Interactions: tests button clicks and modal
|
||||
// User Interactions: tests button clicks
|
||||
describe('User Interactions', () => {
|
||||
it('should open secret key modal when API key button is clicked', () => {
|
||||
renderWithProviders(<Card {...defaultProps} />)
|
||||
|
||||
// Modal should not be visible before clicking
|
||||
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
|
||||
|
||||
const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/).closest('button')
|
||||
fireEvent.click(apiKeyButton!)
|
||||
|
||||
// Modal should appear after clicking
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close secret key modal when onClose is called', () => {
|
||||
it('should call onOpenSecretKeyModal when API key button is clicked', () => {
|
||||
renderWithProviders(<Card {...defaultProps} />)
|
||||
|
||||
const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/).closest('button')
|
||||
fireEvent.click(apiKeyButton!)
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('close'))
|
||||
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
|
||||
expect(onOpenSecretKeyModal).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render API reference as a link', () => {
|
||||
@ -148,20 +133,20 @@ describe('Card (Service API)', () => {
|
||||
// Edge Cases: tests empty/long URLs
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty apiBaseUrl', () => {
|
||||
renderWithProviders(<Card apiBaseUrl="" />)
|
||||
renderWithProviders(<Card apiBaseUrl="" onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
// Should still render the structure
|
||||
expect(screen.getByText(/serviceApi\.card\.endpoint/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long apiBaseUrl', () => {
|
||||
const longUrl = `https://api.dify.ai/${'path/'.repeat(50)}`
|
||||
renderWithProviders(<Card apiBaseUrl={longUrl} />)
|
||||
renderWithProviders(<Card apiBaseUrl={longUrl} onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
expect(screen.getByText(longUrl)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle apiBaseUrl with special characters', () => {
|
||||
const specialUrl = 'https://api.dify.ai/v1?key=value&foo=bar'
|
||||
renderWithProviders(<Card apiBaseUrl={specialUrl} />)
|
||||
renderWithProviders(<Card apiBaseUrl={specialUrl} onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
expect(screen.getByText(specialUrl)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { Popover } from '@langgenius/dify-ui/popover'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Component Imports (after mocks)
|
||||
|
||||
import Card from '../card'
|
||||
import ServiceApi from '../index'
|
||||
|
||||
@ -43,7 +42,8 @@ vi.mock('@/app/components/develop/secret-key/secret-key-modal', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// ServiceApi Component Tests
|
||||
const renderCard = (ui: React.ReactElement) =>
|
||||
render(<Popover open>{ui}</Popover>)
|
||||
|
||||
describe('ServiceApi', () => {
|
||||
beforeEach(() => {
|
||||
@ -80,18 +80,15 @@ describe('ServiceApi', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Props Variations Tests
|
||||
describe('Props Variations', () => {
|
||||
it('should show green Indicator when apiBaseUrl is provided', () => {
|
||||
it('should show Indicator when apiBaseUrl is provided', () => {
|
||||
const { container } = render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
// When apiBaseUrl is truthy, Indicator color is green
|
||||
const triggerContainer = container.querySelector('.relative.flex.h-8')
|
||||
expect(triggerContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show yellow Indicator when apiBaseUrl is empty', () => {
|
||||
it('should show Indicator when apiBaseUrl is empty', () => {
|
||||
const { container } = render(<ServiceApi apiBaseUrl="" />)
|
||||
// When apiBaseUrl is falsy, Indicator color is yellow
|
||||
const triggerContainer = container.querySelector('.relative.flex.h-8')
|
||||
expect(triggerContainer).toBeInTheDocument()
|
||||
})
|
||||
@ -110,28 +107,7 @@ describe('ServiceApi', () => {
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should toggle popup open state on click', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]')
|
||||
expect(trigger).toBeInTheDocument()
|
||||
|
||||
if (trigger)
|
||||
await user.click(trigger)
|
||||
|
||||
// After click, the Card should be rendered
|
||||
})
|
||||
|
||||
it('should apply hover styles on trigger', () => {
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const trigger = screen.getByText(/serviceApi\.title/i).closest('div[class*="cursor-pointer"]')
|
||||
expect(trigger).toHaveClass('cursor-pointer')
|
||||
})
|
||||
|
||||
it('should toggle open state from false to true on first click', async () => {
|
||||
it('should open popup on trigger click', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
@ -140,56 +116,13 @@ describe('ServiceApi', () => {
|
||||
if (trigger)
|
||||
await user.click(trigger)
|
||||
|
||||
// Card should be visible after clicking
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should toggle open state back to false on second click', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]')
|
||||
if (trigger) {
|
||||
await user.click(trigger) // open
|
||||
await user.click(trigger) // close
|
||||
}
|
||||
|
||||
// Component should handle the toggle without errors
|
||||
})
|
||||
|
||||
it('should apply open state styling when popup is open', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]')
|
||||
if (trigger)
|
||||
await user.click(trigger)
|
||||
|
||||
// When open, the trigger should have hover background class
|
||||
})
|
||||
})
|
||||
|
||||
// Portal and Card Integration Tests
|
||||
describe('Portal and Card Integration', () => {
|
||||
it('should render Card component inside portal when open', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]')
|
||||
if (trigger)
|
||||
await user.click(trigger)
|
||||
|
||||
// Wait for portal content to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass apiBaseUrl prop to Card component', async () => {
|
||||
const user = userEvent.setup()
|
||||
const testUrl = 'https://test-api.example.com'
|
||||
@ -204,38 +137,9 @@ describe('ServiceApi', () => {
|
||||
expect(screen.getByText(testUrl)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should use correct portal placement configuration', () => {
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
// PortalToFollowElem is configured with placement="top-start"
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use correct portal offset configuration', () => {
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
// PortalToFollowElem is configured with offset={{ mainAxis: 4, crossAxis: -4 }}
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle rapid toggle clicks gracefully', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]')
|
||||
if (trigger) {
|
||||
// Rapid clicks
|
||||
await user.click(trigger)
|
||||
await user.click(trigger)
|
||||
await user.click(trigger)
|
||||
}
|
||||
|
||||
// Component should handle state changes without errors
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render correctly with empty apiBaseUrl', () => {
|
||||
render(<ServiceApi apiBaseUrl="" />)
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
@ -248,391 +152,60 @@ describe('ServiceApi', () => {
|
||||
|
||||
rerender(<ServiceApi apiBaseUrl="https://new-api.example.com" />)
|
||||
|
||||
// Component should still render after prop change
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined-like apiBaseUrl values', () => {
|
||||
// Empty string is the closest to undefined for this prop
|
||||
render(<ServiceApi apiBaseUrl="" />)
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized with React.memo', () => {
|
||||
const { rerender } = render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
rerender(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not re-render unnecessarily with same props', () => {
|
||||
const { rerender } = render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
rerender(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
rerender(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update when apiBaseUrl prop changes', () => {
|
||||
const { rerender } = render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
rerender(<ServiceApi apiBaseUrl="https://new-api.example.com" />)
|
||||
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Card Component Tests
|
||||
|
||||
describe('Card (service-api)', () => {
|
||||
const onOpenSecretKeyModal = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
renderCard(<Card apiBaseUrl="https://api.example.com" onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display card title', () => {
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display enabled status', () => {
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
expect(screen.getByText(/serviceApi\.enabled/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render endpoint label', () => {
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
expect(screen.getByText(/serviceApi\.card\.endpoint/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display apiBaseUrl in endpoint field', () => {
|
||||
const testUrl = 'https://api.example.com'
|
||||
render(<Card apiBaseUrl={testUrl} />)
|
||||
renderCard(<Card apiBaseUrl={testUrl} onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
expect(screen.getByText(testUrl)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Indicator component', () => {
|
||||
const { container } = render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
// Card container should be present
|
||||
const cardContainer = container.querySelector('.flex.w-\\[360px\\]')
|
||||
expect(cardContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render API Key button', () => {
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
renderCard(<Card apiBaseUrl="https://api.example.com" onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
expect(screen.getByText(/serviceApi\.card\.apiKey/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render API Reference button', () => {
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
renderCard(<Card apiBaseUrl="https://api.example.com" onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
expect(screen.getByText(/serviceApi\.card\.apiReference/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render CopyFeedback component for endpoint', () => {
|
||||
const { container } = render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
// CopyFeedback should be in the endpoint section
|
||||
const copyButton = container.querySelector('[class*="bg-components-input-bg-normal"]')
|
||||
expect(copyButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ApiAggregate icon in header', () => {
|
||||
const { container } = render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
const icon = container.querySelector('svg')
|
||||
expect(icon).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props Variations Tests
|
||||
describe('Props Variations', () => {
|
||||
it('should show green Indicator when apiBaseUrl is provided', () => {
|
||||
const { container } = render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
const cardContainer = container.querySelector('.flex.w-\\[360px\\]')
|
||||
expect(cardContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show yellow Indicator when apiBaseUrl is empty', () => {
|
||||
const { container } = render(<Card apiBaseUrl="" />)
|
||||
const cardContainer = container.querySelector('.flex.w-\\[360px\\]')
|
||||
expect(cardContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display different apiBaseUrl values correctly', () => {
|
||||
const testUrls = [
|
||||
'https://api.example.com',
|
||||
'https://localhost:3000',
|
||||
'https://api.production.example.com/v1',
|
||||
]
|
||||
|
||||
testUrls.forEach((url) => {
|
||||
const { unmount } = render(<Card apiBaseUrl={url} />)
|
||||
expect(screen.getByText(url)).toBeInTheDocument()
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty apiBaseUrl', () => {
|
||||
render(<Card apiBaseUrl="" />)
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should truncate long apiBaseUrl', () => {
|
||||
const longUrl = 'https://api.example.com/v1/very/long/path/to/endpoint/that/should/truncate'
|
||||
const { container } = render(<Card apiBaseUrl={longUrl} />)
|
||||
const truncateElement = container.querySelector('.truncate')
|
||||
expect(truncateElement).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should open SecretKeyModal when API Key button is clicked', async () => {
|
||||
it('should call onOpenSecretKeyModal when API Key button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button')
|
||||
expect(apiKeyButton).toBeInTheDocument()
|
||||
|
||||
if (apiKeyButton)
|
||||
await user.click(apiKeyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should close SecretKeyModal when close button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
renderCard(<Card apiBaseUrl="https://api.example.com" onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
|
||||
const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button')
|
||||
if (apiKeyButton)
|
||||
await user.click(apiKeyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const closeButton = screen.getByTestId('close-modal-btn')
|
||||
await user.click(closeButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(onOpenSecretKeyModal).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should have correct href for API Reference link', () => {
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
renderCard(<Card apiBaseUrl="https://api.example.com" onOpenSecretKeyModal={onOpenSecretKeyModal} />)
|
||||
|
||||
const apiRefLink = screen.getByText(/serviceApi\.card\.apiReference/i).closest('a')
|
||||
expect(apiRefLink).toHaveAttribute('href', 'https://docs.dify.ai/api-reference/datasets')
|
||||
})
|
||||
|
||||
it('should open API Reference in new tab', () => {
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const apiRefLink = screen.getByText(/serviceApi\.card\.apiReference/i).closest('a')
|
||||
expect(apiRefLink).toHaveAttribute('target', '_blank')
|
||||
expect(apiRefLink).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('should toggle modal visibility correctly', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
// Initially modal should not be visible
|
||||
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
|
||||
|
||||
const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button')
|
||||
if (apiKeyButton)
|
||||
await user.click(apiKeyButton)
|
||||
|
||||
// Modal should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const closeButton = screen.getByTestId('close-modal-btn')
|
||||
await user.click(closeButton)
|
||||
|
||||
// Modal should not be visible again
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Modal State Tests
|
||||
describe('Modal State', () => {
|
||||
it('should initialize with modal closed', () => {
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open modal on handleOpenSecretKeyModal', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button')
|
||||
if (apiKeyButton)
|
||||
await user.click(apiKeyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should close modal on handleCloseSecretKeyModal', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
// Open modal first
|
||||
const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button')
|
||||
if (apiKeyButton)
|
||||
await user.click(apiKeyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const closeButton = screen.getByTestId('close-modal-btn')
|
||||
await user.click(closeButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle multiple open/close cycles', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button')
|
||||
|
||||
// First cycle
|
||||
if (apiKeyButton)
|
||||
await user.click(apiKeyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.click(screen.getByTestId('close-modal-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Second cycle
|
||||
if (apiKeyButton)
|
||||
await user.click(apiKeyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty apiBaseUrl gracefully', () => {
|
||||
render(<Card apiBaseUrl="" />)
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
// Endpoint field should show empty string
|
||||
})
|
||||
|
||||
it('should handle very long apiBaseUrl', () => {
|
||||
const longUrl = 'https://'.concat('a'.repeat(500), '.com')
|
||||
render(<Card apiBaseUrl={longUrl} />)
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in apiBaseUrl', () => {
|
||||
const specialUrl = 'https://api.example.com/path?query=test¶m=value#anchor'
|
||||
render(<Card apiBaseUrl={specialUrl} />)
|
||||
expect(screen.getByText(specialUrl)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render without errors when all buttons are clickable', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button')
|
||||
if (apiKeyButton)
|
||||
await user.click(apiKeyButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.click(screen.getByTestId('close-modal-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('secret-key-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Component should still be functional
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized with React.memo', () => {
|
||||
const { rerender } = render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
rerender(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use useCallback for handlers', () => {
|
||||
const { rerender } = render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
rerender(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
// Component should render without issues with memoized callbacks
|
||||
expect(screen.getByText(/serviceApi\.card\.apiKey/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update when apiBaseUrl prop changes', () => {
|
||||
const { rerender } = render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
expect(screen.getByText('https://api.example.com')).toBeInTheDocument()
|
||||
|
||||
rerender(<Card apiBaseUrl="https://new-api.example.com" />)
|
||||
|
||||
expect(screen.getByText('https://new-api.example.com')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Copy Functionality Tests
|
||||
describe('Copy Functionality', () => {
|
||||
it('should render CopyFeedback component for apiBaseUrl', () => {
|
||||
const { container } = render(<Card apiBaseUrl="https://api.example.com" />)
|
||||
const copyContainer = container.querySelector('[class*="bg-components-input-bg-normal"]')
|
||||
expect(copyContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass apiBaseUrl to CopyFeedback component', () => {
|
||||
const testUrl = 'https://api.example.com'
|
||||
render(<Card apiBaseUrl={testUrl} />)
|
||||
// The URL should be displayed in the copy section
|
||||
expect(screen.getByText(testUrl)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -641,78 +214,33 @@ describe('ServiceApi Integration', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should open Card popup and display endpoint', async () => {
|
||||
const user = userEvent.setup()
|
||||
const testUrl = 'https://api.example.com'
|
||||
|
||||
render(<ServiceApi apiBaseUrl={testUrl} />)
|
||||
|
||||
// Open popup
|
||||
const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]')
|
||||
if (trigger)
|
||||
await user.click(trigger)
|
||||
|
||||
// Wait for Card to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(testUrl)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should complete full workflow: open -> view endpoint -> access API key', async () => {
|
||||
it('should close popover and open modal when API Key button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
// Open popup
|
||||
// Open popover
|
||||
const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]')
|
||||
if (trigger)
|
||||
await user.click(trigger)
|
||||
|
||||
// Verify Card content
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/serviceApi\.card\.title/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/serviceApi\.enabled/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/serviceApi\.card\.apiKey/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Open API Key modal
|
||||
// Click API Key button (wrapped by PopoverClose)
|
||||
const apiKeyButton = screen.getByText(/serviceApi\.card\.apiKey/i).closest('button')
|
||||
if (apiKeyButton)
|
||||
await user.click(apiKeyButton)
|
||||
|
||||
// Verify modal appears
|
||||
// Modal should appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('secret-key-modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should navigate to API Reference from Card', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
|
||||
// Open popup
|
||||
const trigger = screen.getByText(/serviceApi\.title/i).closest('[class*="cursor-pointer"]')
|
||||
if (trigger)
|
||||
await user.click(trigger)
|
||||
|
||||
// Wait for Card to appear
|
||||
// Popover should be closed — Card title no longer in document
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/serviceApi\.card\.apiReference/i)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/serviceApi\.card\.title/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Verify link
|
||||
const apiRefLink = screen.getByText(/serviceApi\.card\.apiReference/i).closest('a')
|
||||
expect(apiRefLink).toHaveAttribute('href', 'https://docs.dify.ai/api-reference/datasets')
|
||||
})
|
||||
|
||||
it('should reflect apiBaseUrl status in Indicator color', () => {
|
||||
// With URL - should be green
|
||||
const { rerender } = render(<ServiceApi apiBaseUrl="https://api.example.com" />)
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
|
||||
// Without URL - should be yellow
|
||||
rerender(<ServiceApi apiBaseUrl="" />)
|
||||
expect(screen.getByText(/serviceApi\.title/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,35 +1,27 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { PopoverClose } from '@langgenius/dify-ui/popover'
|
||||
import { RiBookOpenLine, RiKey2Line } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import { ApiAggregate } from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import SecretKeyModal from '@/app/components/develop/secret-key/secret-key-modal'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
|
||||
import Link from '@/next/link'
|
||||
|
||||
type CardProps = {
|
||||
apiBaseUrl: string
|
||||
onOpenSecretKeyModal: () => void
|
||||
}
|
||||
|
||||
const Card = ({
|
||||
apiBaseUrl,
|
||||
onOpenSecretKeyModal,
|
||||
}: CardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isSecretKeyModalVisible, setIsSecretKeyModalVisible] = useState(false)
|
||||
|
||||
const apiReferenceUrl = useDatasetApiAccessUrl()
|
||||
|
||||
const handleOpenSecretKeyModal = useCallback(() => {
|
||||
setIsSecretKeyModalVisible(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseSecretKeyModal = useCallback(() => {
|
||||
setIsSecretKeyModalVisible(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex w-[360px] flex-col rounded-xl border border-components-panel-border bg-components-panel-bg shadow-lg shadow-shadow-shadow-1">
|
||||
<div className="flex flex-col gap-y-3 p-4">
|
||||
@ -74,17 +66,21 @@ const Card = ({
|
||||
</div>
|
||||
{/* Actions */}
|
||||
<div className="flex gap-x-1 border-t-[0.5px] border-divider-subtle p-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="gap-x-px text-text-tertiary"
|
||||
onClick={handleOpenSecretKeyModal}
|
||||
>
|
||||
<RiKey2Line className="size-3.5 shrink-0" />
|
||||
<span className="px-[3px] system-xs-medium">
|
||||
{t('serviceApi.card.apiKey', { ns: 'dataset' })}
|
||||
</span>
|
||||
</Button>
|
||||
<PopoverClose
|
||||
render={(
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="gap-x-px text-text-tertiary"
|
||||
onClick={onOpenSecretKeyModal}
|
||||
>
|
||||
<RiKey2Line className="size-3.5 shrink-0" />
|
||||
<span className="px-[3px] system-xs-medium">
|
||||
{t('serviceApi.card.apiKey', { ns: 'dataset' })}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<Link
|
||||
href={apiReferenceUrl}
|
||||
target="_blank"
|
||||
@ -102,10 +98,6 @@ const Card = ({
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<SecretKeyModal
|
||||
isShow={isSecretKeyModalVisible}
|
||||
onClose={handleCloseSecretKeyModal}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SecretKeyModal from '@/app/components/develop/secret-key/secret-key-modal'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Card from './card'
|
||||
|
||||
@ -15,6 +16,15 @@ const ServiceApi = ({
|
||||
}: ServiceApiProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isSecretKeyModalVisible, setIsSecretKeyModalVisible] = useState(false)
|
||||
|
||||
const handleOpenSecretKeyModal = useCallback(() => {
|
||||
setIsSecretKeyModalVisible(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseSecretKeyModal = useCallback(() => {
|
||||
setIsSecretKeyModalVisible(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -49,9 +59,14 @@ const ServiceApi = ({
|
||||
>
|
||||
<Card
|
||||
apiBaseUrl={apiBaseUrl}
|
||||
onOpenSecretKeyModal={handleOpenSecretKeyModal}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<SecretKeyModal
|
||||
isShow={isSecretKeyModalVisible}
|
||||
onClose={handleCloseSecretKeyModal}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -99,6 +99,14 @@ vi.mock('../../external-api/external-api-panel', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock SecretKeyModal — it depends on user profile context and service APIs
|
||||
// not configured in this test. ServiceApi always mounts the modal (controlled
|
||||
// by `isShow`) so we provide a lightweight stub.
|
||||
vi.mock('@/app/components/develop/secret-key/secret-key-modal', () => ({
|
||||
default: ({ isShow }: { isShow: boolean }) =>
|
||||
isShow ? <div data-testid="secret-key-modal" /> : null,
|
||||
}))
|
||||
|
||||
// Mock TagManagementModal
|
||||
vi.mock('@/app/components/base/tag-management', () => ({
|
||||
default: () => <div data-testid="tag-management-modal" />,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import SetURL from '../setURL'
|
||||
|
||||
@ -53,6 +53,15 @@ describe('SetURL', () => {
|
||||
const input = screen.getByLabelText('plugin.installFromGitHub.gitHubRepo')
|
||||
expect(input).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should auto-focus the input on mount', async () => {
|
||||
render(<SetURL {...defaultProps} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await waitFor(() => {
|
||||
expect(input).toHaveFocus()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
|
||||
@ -13,6 +13,18 @@ type SetURLProps = {
|
||||
|
||||
const SetURL: React.FC<SetURLProps> = ({ repoUrl, onChange, onNext, onCancel }) => {
|
||||
const { t } = useTranslation()
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
// Focus the input after the dropdown's focus-return animation settles.
|
||||
// Using rAF avoids racing the DropdownMenu FloatingFocusManager that returns
|
||||
// focus to the trigger on close.
|
||||
React.useEffect(() => {
|
||||
const frame = requestAnimationFrame(() => {
|
||||
inputRef.current?.focus()
|
||||
})
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
@ -22,6 +34,7 @@ const SetURL: React.FC<SetURLProps> = ({ repoUrl, onChange, onNext, onCancel })
|
||||
<span className="system-sm-semibold">{t('installFromGitHub.gitHubRepo', { ns: 'plugin' })}</span>
|
||||
</label>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
id="repoUrl"
|
||||
name="repoUrl"
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import DebugInfo from '../debug-info'
|
||||
|
||||
@ -15,27 +16,6 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
useDebugKey: () => mockDebugKey,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children }: { children: React.ReactNode }) => <button data-testid="debug-button">{children}</button>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({
|
||||
children,
|
||||
disabled,
|
||||
popupContent,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
popupContent: React.ReactNode
|
||||
}) => (
|
||||
<div>
|
||||
{children}
|
||||
{!disabled && <div data-testid="tooltip-content">{popupContent}</div>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../base/key-value-item', () => ({
|
||||
default: ({
|
||||
label,
|
||||
@ -68,16 +48,31 @@ describe('DebugInfo', () => {
|
||||
expect(container.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
it('renders debug metadata and masks the key when info is available', () => {
|
||||
it('renders a disabled trigger when debug info is unavailable', () => {
|
||||
render(<DebugInfo />)
|
||||
|
||||
const trigger = screen.getByRole('button')
|
||||
expect(trigger).toBeDisabled()
|
||||
})
|
||||
|
||||
it('opens a popover with debug metadata and masks the key when info is available', async () => {
|
||||
mockDebugKey.data = {
|
||||
host: '127.0.0.1',
|
||||
port: 5001,
|
||||
key: '12345678abcdefghijklmnopqrst87654321',
|
||||
}
|
||||
|
||||
const user = userEvent.setup()
|
||||
render(<DebugInfo />)
|
||||
|
||||
expect(screen.getByTestId('debug-button')).toBeInTheDocument()
|
||||
const trigger = screen.getByRole('button')
|
||||
expect(trigger).toBeEnabled()
|
||||
|
||||
// Popover is closed initially — content not rendered yet
|
||||
expect(screen.queryByText('plugin.debugInfo.title')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(trigger)
|
||||
|
||||
expect(screen.getByText('plugin.debugInfo.title')).toBeInTheDocument()
|
||||
expect(screen.getByRole('link')).toHaveAttribute(
|
||||
'href',
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
RiBugLine,
|
||||
} from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useDebugKey } from '@/service/use-plugins'
|
||||
import KeyValueItem from '../base/key-value-item'
|
||||
@ -25,41 +25,54 @@ const DebugInfo: FC = () => {
|
||||
if (isLoading)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
triggerMethod="click"
|
||||
disabled={!info}
|
||||
popupContent={(
|
||||
<>
|
||||
<div className="flex items-center gap-1 self-stretch">
|
||||
<span className="flex shrink-0 grow basis-0 flex-col items-start justify-center system-sm-semibold text-text-secondary">{t(`${i18nPrefix}.title`, { ns: 'plugin' })}</span>
|
||||
<a href={docLink('/develop-plugin/features-and-specs/plugin-types/remote-debug-a-plugin')} target="_blank" className="flex cursor-pointer items-center gap-0.5 text-text-accent-light-mode-only">
|
||||
<span className="system-xs-medium">{t(`${i18nPrefix}.viewDocs`, { ns: 'plugin' })}</span>
|
||||
<RiArrowRightUpLine className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<KeyValueItem
|
||||
label="URL"
|
||||
value={`${info?.host}:${info?.port}`}
|
||||
/>
|
||||
<KeyValueItem
|
||||
label="Key"
|
||||
value={info?.key || ''}
|
||||
maskedValue={maskedKey}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
popupClassName="flex flex-col items-start w-[256px] px-4 py-3.5 gap-1 border border-components-panel-border
|
||||
rounded-xl bg-components-tooltip-bg shadows-shadow-lg z-50"
|
||||
asChild={false}
|
||||
position="bottom"
|
||||
>
|
||||
<Button className="h-full w-full p-2 text-components-button-secondary-text">
|
||||
if (!info) {
|
||||
return (
|
||||
<Button className="h-full w-full p-2 text-components-button-secondary-text" disabled>
|
||||
<RiBugLine className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<Button className="h-full w-full p-2 text-components-button-secondary-text">
|
||||
<RiBugLine className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom"
|
||||
popupClassName="flex w-[256px] flex-col items-start gap-1 rounded-xl border border-components-panel-border bg-components-tooltip-bg px-4 py-3.5 shadow-lg"
|
||||
>
|
||||
<div className="flex items-center gap-1 self-stretch">
|
||||
<span className="flex shrink-0 grow basis-0 flex-col items-start justify-center system-sm-semibold text-text-secondary">
|
||||
{t(`${i18nPrefix}.title`, { ns: 'plugin' })}
|
||||
</span>
|
||||
<a
|
||||
href={docLink('/develop-plugin/features-and-specs/plugin-types/remote-debug-a-plugin')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex cursor-pointer items-center gap-0.5 text-text-accent-light-mode-only"
|
||||
>
|
||||
<span className="system-xs-medium">{t(`${i18nPrefix}.viewDocs`, { ns: 'plugin' })}</span>
|
||||
<RiArrowRightUpLine className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<KeyValueItem
|
||||
label="URL"
|
||||
value={`${info.host}:${info.port}`}
|
||||
/>
|
||||
<KeyValueItem
|
||||
label="Key"
|
||||
value={info.key || ''}
|
||||
maskedValue={maskedKey}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user