mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
refactor(logs): scope page data and replay to the owning app (#43260)
This commit is contained in:
8 files changed
+175
-140
No files matched your search
@@ -1,13 +1,21 @@
|
||||
import * as React from 'react'
|
||||
import { Suspense } from 'react'
|
||||
import Main from '@/app/components/app/log-annotation'
|
||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||
import { SkeletonContainer, SkeletonRectangle } from '@/app/components/base/skeleton'
|
||||
|
||||
export type IProps = {
|
||||
params: Promise<{ appId: string }>
|
||||
export default async function Page({ params }: { params: Promise<{ appId: string }> }) {
|
||||
const { appId } = await params
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<SkeletonContainer className="gap-4 p-6" aria-busy="true">
|
||||
<SkeletonRectangle className="h-6 w-48" />
|
||||
<SkeletonRectangle className="h-10 w-full" />
|
||||
<SkeletonRectangle className="h-96 w-full" />
|
||||
</SkeletonContainer>
|
||||
}
|
||||
>
|
||||
<Main appId={appId} pageType={PageType.annotation} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
const Logs = async () => {
|
||||
return <Main pageType={PageType.annotation} />
|
||||
}
|
||||
|
||||
export default Logs
|
||||
@@ -1,9 +1,21 @@
|
||||
import * as React from 'react'
|
||||
import { Suspense } from 'react'
|
||||
import Main from '@/app/components/app/log-annotation'
|
||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||
import { SkeletonContainer, SkeletonRectangle } from '@/app/components/base/skeleton'
|
||||
|
||||
const Logs = async () => {
|
||||
return <Main pageType={PageType.log} />
|
||||
export default async function Page({ params }: { params: Promise<{ appId: string }> }) {
|
||||
const { appId } = await params
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<SkeletonContainer className="gap-4 p-6" aria-busy="true">
|
||||
<SkeletonRectangle className="h-6 w-48" />
|
||||
<SkeletonRectangle className="h-10 w-full" />
|
||||
<SkeletonRectangle className="h-96 w-full" />
|
||||
</SkeletonContainer>
|
||||
}
|
||||
>
|
||||
<Main appId={appId} pageType={PageType.log} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logs
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { Suspense } from 'react'
|
||||
import ErrorBoundary from '@/app/components/base/error-boundary'
|
||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { createAppDetailFixture, createAppSiteFixture } from '@/test/fixtures/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import LogAnnotation from '../index'
|
||||
@@ -33,31 +36,49 @@ const createMockApp = (overrides: Partial<AppDetailWithSite> = {}): AppDetailWit
|
||||
...overrides,
|
||||
})
|
||||
|
||||
let appDetail: AppDetailWithSite | undefined
|
||||
const render = (ui: Parameters<typeof renderWithConsoleQuery>[0]) => {
|
||||
const queryClient = createConsoleQueryClient()
|
||||
if (appDetail)
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: appDetail.id } } }),
|
||||
appDetail,
|
||||
)
|
||||
return renderWithConsoleQuery(<Suspense fallback={<div>Loading logs</div>}>{ui}</Suspense>, {
|
||||
queryClient,
|
||||
})
|
||||
}
|
||||
|
||||
vi.mock('@/service/base', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/base')>()),
|
||||
request: vi.fn(() => new Promise(() => {})),
|
||||
}))
|
||||
|
||||
describe('LogAnnotation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAppStore.setState({ appDetail: createMockApp() })
|
||||
appDetail = createMockApp()
|
||||
})
|
||||
|
||||
// Rendering behavior
|
||||
describe('Rendering', () => {
|
||||
it('should render loading state when app detail is missing', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: undefined })
|
||||
appDetail = undefined
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
render(<LogAnnotation appId="app-123" pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument()
|
||||
expect(screen.getByText('Loading logs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render log content without the old page tabs', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) })
|
||||
appDetail = createMockApp({ mode: AppModeEnum.CHAT })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
render(<LogAnnotation appId="app-123" pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('region', { name: 'App log' })).toBeInTheDocument()
|
||||
@@ -67,10 +88,10 @@ describe('LogAnnotation', () => {
|
||||
|
||||
it('should render completion logs without the old page tabs', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.COMPLETION }) })
|
||||
appDetail = createMockApp({ mode: AppModeEnum.COMPLETION })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
render(<LogAnnotation appId="app-123" pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('region', { name: 'App log' })).toBeInTheDocument()
|
||||
@@ -80,10 +101,10 @@ describe('LogAnnotation', () => {
|
||||
|
||||
it('should hide tabs and render workflow log in workflow mode', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.WORKFLOW }) })
|
||||
appDetail = createMockApp({ mode: AppModeEnum.WORKFLOW })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
render(<LogAnnotation appId="app-123" pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('appLog.title')).not.toBeInTheDocument()
|
||||
@@ -95,10 +116,10 @@ describe('LogAnnotation', () => {
|
||||
describe('Props', () => {
|
||||
it('should render log content when page type is log', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) })
|
||||
appDetail = createMockApp({ mode: AppModeEnum.CHAT })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
render(<LogAnnotation appId="app-123" pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('region', { name: 'App log' })).toBeInTheDocument()
|
||||
@@ -107,10 +128,10 @@ describe('LogAnnotation', () => {
|
||||
|
||||
it('should render annotation content when page type is annotation', () => {
|
||||
// Arrange
|
||||
useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) })
|
||||
appDetail = createMockApp({ mode: AppModeEnum.CHAT })
|
||||
|
||||
// Act
|
||||
render(<LogAnnotation pageType={PageType.annotation} />)
|
||||
render(<LogAnnotation appId="app-123" pageType={PageType.annotation} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('region', { name: 'Annotation log' })).toBeInTheDocument()
|
||||
@@ -118,3 +139,45 @@ describe('LogAnnotation', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the destination log mode and app identity after navigation', () => {
|
||||
appDetail = createMockApp({ mode: 'chat' })
|
||||
const { queryClient, rerender } = render(
|
||||
<LogAnnotation appId="app-123" pageType={PageType.log} />,
|
||||
)
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.apps.byAppId.get.queryKey({ input: { params: { app_id: 'workflow-app' } } }),
|
||||
createMockApp({ id: 'workflow-app', mode: 'workflow' }),
|
||||
)
|
||||
expect(screen.getByRole('region', { name: 'App log' })).toHaveTextContent('app-123')
|
||||
rerender(<LogAnnotation appId="workflow-app" pageType={PageType.log} />)
|
||||
expect(screen.getByRole('region', { name: 'Workflow log' })).toHaveTextContent('workflow-app')
|
||||
expect(screen.queryByRole('region', { name: 'App log' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('surfaces an initial detail error instead of staying in the loading state', async () => {
|
||||
const queryClient = createConsoleQueryClient()
|
||||
const queryKey = consoleQuery.apps.byAppId.get.queryKey({
|
||||
input: { params: { app_id: 'unavailable-app' } },
|
||||
})
|
||||
await queryClient
|
||||
.query({ queryKey, queryFn: () => Promise.reject(new Error('Detail unavailable')) })
|
||||
.catch(() => {})
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
try {
|
||||
renderWithConsoleQuery(
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<div>Loading logs</div>}>
|
||||
<LogAnnotation appId="unavailable-app" pageType={PageType.log} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>,
|
||||
{ queryClient },
|
||||
)
|
||||
expect(
|
||||
await screen.findByRole('button', { name: 'common.errorBoundary.tryAgain' }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.queryByText('Loading logs')).not.toBeInTheDocument()
|
||||
} finally {
|
||||
consoleError.mockRestore()
|
||||
}
|
||||
})
|
||||
@@ -1,28 +1,25 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import Annotation from '@/app/components/app/annotation'
|
||||
import Log from '@/app/components/app/log'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import WorkflowLog from '@/app/components/app/workflow-log'
|
||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
type Props = Readonly<{
|
||||
appId: string
|
||||
pageType: PageType
|
||||
}>
|
||||
|
||||
const LogAnnotation: FC<Props> = ({ pageType }) => {
|
||||
const appDetail = useAppStore((state) => state.appDetail)
|
||||
|
||||
if (!appDetail) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-background-body">
|
||||
<LoadingPlaceholder />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const LogAnnotation: FC<Props> = ({ appId, pageType }) => {
|
||||
const { data: appDetail } = useSuspenseQuery(
|
||||
consoleQuery.apps.byAppId.get.queryOptions({
|
||||
input: { params: { app_id: appId } },
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col px-6 pt-3">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen'
|
||||
/**
|
||||
* DetailPanel Component Tests
|
||||
*
|
||||
@@ -10,8 +9,6 @@ import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.g
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { createAppDetailFixture, createAppSiteFixture } from '@/test/fixtures/app'
|
||||
import DetailPanel from '../detail'
|
||||
|
||||
// ============================================================================
|
||||
@@ -54,29 +51,13 @@ vi.mock('ahooks', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Factories
|
||||
// ============================================================================
|
||||
|
||||
const createMockApp = (overrides: Partial<AppDetailWithSite> = {}): AppDetailWithSite =>
|
||||
createAppDetailFixture({
|
||||
id: 'test-app-id',
|
||||
name: 'Test App',
|
||||
mode: 'workflow',
|
||||
site: createAppSiteFixture({ access_token: 'token', app_base_url: 'https://example.com' }),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('DetailPanel', () => {
|
||||
const defaultOnClose = vi.fn()
|
||||
let appId = 'test-app-id'
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAppStore.setState({ appDetail: createMockApp() })
|
||||
appId = 'test-app-id'
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -84,21 +65,21 @@ describe('DetailPanel', () => {
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render workflow title', () => {
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID="run-123" onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByText('appLog.runDetail.workflowTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID="run-123" onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'common.operation.close' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Run component with correct URLs', () => {
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-456' }) })
|
||||
appId = 'app-456'
|
||||
|
||||
render(<DetailPanel runID="run-789" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID="run-789" onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByTestId('workflow-run')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('run-detail-url')).toHaveTextContent(
|
||||
@@ -110,7 +91,7 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should render WorkflowContextProvider wrapper', () => {
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID="run-123" onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByTestId('workflow-context-provider')).toBeInTheDocument()
|
||||
})
|
||||
@@ -121,7 +102,7 @@ describe('DetailPanel', () => {
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Props', () => {
|
||||
it('should not render replay button when canReplay is false (default)', () => {
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID="run-123" onClose={defaultOnClose} />)
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'appLog.runDetail.testWithParams' }),
|
||||
@@ -129,7 +110,9 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should render replay button when canReplay is true', () => {
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} canReplay={true} />)
|
||||
render(
|
||||
<DetailPanel appId={appId} runID="run-123" onClose={defaultOnClose} canReplay={true} />,
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' }),
|
||||
@@ -137,7 +120,7 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should use empty URL when runID is empty', () => {
|
||||
render(<DetailPanel runID="" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID="" onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByTestId('run-detail-url')).toHaveTextContent('')
|
||||
expect(screen.getByTestId('tracing-list-url')).toHaveTextContent('')
|
||||
@@ -152,7 +135,7 @@ describe('DetailPanel', () => {
|
||||
const user = userEvent.setup()
|
||||
const onClose = vi.fn()
|
||||
|
||||
render(<DetailPanel runID="run-123" onClose={onClose} />)
|
||||
render(<DetailPanel appId={appId} runID="run-123" onClose={onClose} />)
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
|
||||
@@ -163,9 +146,16 @@ describe('DetailPanel', () => {
|
||||
|
||||
it('should navigate to workflow page with replayRunId when replay button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-replay-test' }) })
|
||||
appId = 'app-replay-test'
|
||||
|
||||
render(<DetailPanel runID="run-to-replay" onClose={defaultOnClose} canReplay={true} />)
|
||||
render(
|
||||
<DetailPanel
|
||||
appId={appId}
|
||||
runID="run-to-replay"
|
||||
onClose={defaultOnClose}
|
||||
canReplay={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
|
||||
await user.click(replayButton)
|
||||
@@ -174,18 +164,6 @@ describe('DetailPanel', () => {
|
||||
'/app/app-replay-test/workflow?replayRunId=run-to-replay',
|
||||
)
|
||||
})
|
||||
|
||||
it('should not navigate when replay clicked but appDetail is missing', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: undefined })
|
||||
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} canReplay={true} />)
|
||||
|
||||
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
|
||||
await user.click(replayButton)
|
||||
|
||||
expect(mockRouterPush).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -193,9 +171,9 @@ describe('DetailPanel', () => {
|
||||
// --------------------------------------------------------------------------
|
||||
describe('URL Generation', () => {
|
||||
it('should generate correct run detail URL', () => {
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'my-app' }) })
|
||||
appId = 'my-app'
|
||||
|
||||
render(<DetailPanel runID="my-run" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID="my-run" onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByTestId('run-detail-url')).toHaveTextContent(
|
||||
'/apps/my-app/workflow-runs/my-run',
|
||||
@@ -203,9 +181,9 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should generate correct tracing list URL', () => {
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'my-app' }) })
|
||||
appId = 'my-app'
|
||||
|
||||
render(<DetailPanel runID="my-run" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID="my-run" onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByTestId('tracing-list-url')).toHaveTextContent(
|
||||
'/apps/my-app/workflow-runs/my-run/node-executions',
|
||||
@@ -213,9 +191,9 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should handle special characters in runID', () => {
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-id' }) })
|
||||
appId = 'app-id'
|
||||
|
||||
render(<DetailPanel runID="run-with-special-123" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID="run-with-special-123" onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByTestId('run-detail-url')).toHaveTextContent(
|
||||
'/apps/app-id/workflow-runs/run-with-special-123',
|
||||
@@ -223,36 +201,12 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Store Integration Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Store Integration', () => {
|
||||
it('should read appDetail from store', () => {
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'store-app-id' }) })
|
||||
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByTestId('run-detail-url')).toHaveTextContent(
|
||||
'/apps/store-app-id/workflow-runs/run-123',
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle undefined appDetail from store gracefully', () => {
|
||||
useAppStore.setState({ appDetail: undefined })
|
||||
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
|
||||
// Run component should still render but with undefined in URL
|
||||
expect(screen.getByTestId('workflow-run')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases (REQUIRED)
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty runID', () => {
|
||||
render(<DetailPanel runID="" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID="" onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByTestId('run-detail-url')).toHaveTextContent('')
|
||||
expect(screen.getByTestId('tracing-list-url')).toHaveTextContent('')
|
||||
@@ -260,9 +214,9 @@ describe('DetailPanel', () => {
|
||||
|
||||
it('should handle very long runID', () => {
|
||||
const longRunId = 'a'.repeat(100)
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-id' }) })
|
||||
appId = 'app-id'
|
||||
|
||||
render(<DetailPanel runID={longRunId} onClose={defaultOnClose} />)
|
||||
render(<DetailPanel appId={appId} runID={longRunId} onClose={defaultOnClose} />)
|
||||
|
||||
expect(screen.getByTestId('run-detail-url')).toHaveTextContent(
|
||||
`/apps/app-id/workflow-runs/${longRunId}`,
|
||||
@@ -270,14 +224,18 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should render replay button with correct aria-label', () => {
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} canReplay={true} />)
|
||||
render(
|
||||
<DetailPanel appId={appId} runID="run-123" onClose={defaultOnClose} canReplay={true} />,
|
||||
)
|
||||
|
||||
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
|
||||
expect(replayButton).toHaveAttribute('aria-label', 'appLog.runDetail.testWithParams')
|
||||
})
|
||||
|
||||
it('should maintain proper component structure', () => {
|
||||
const { container } = render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
const { container } = render(
|
||||
<DetailPanel appId={appId} runID="run-123" onClose={defaultOnClose} />,
|
||||
)
|
||||
|
||||
// Check for main container with flex layout
|
||||
const mainContainer = container.querySelector('.flex.grow.flex-col')
|
||||
@@ -294,7 +252,9 @@ describe('DetailPanel', () => {
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Tooltip', () => {
|
||||
it('should have tooltip on replay button', () => {
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} canReplay={true} />)
|
||||
render(
|
||||
<DetailPanel appId={appId} runID="run-123" onClose={defaultOnClose} canReplay={true} />,
|
||||
)
|
||||
|
||||
// The replay button should be wrapped in TooltipPlus
|
||||
const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' })
|
||||
|
||||
@@ -13,7 +13,6 @@ import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } fr
|
||||
import type { AppModeEnum } from '@/types/app'
|
||||
import { render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { APP_PAGE_LIMIT } from '@/config'
|
||||
import { WorkflowRunTriggeredFrom } from '@/models/log'
|
||||
import { createAppDetailFixture, createAppSiteFixture } from '@/test/fixtures/app'
|
||||
@@ -150,7 +149,6 @@ describe('WorkflowAppLogList', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAppStore.setState({ appDetail: createMockApp() })
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -447,7 +445,7 @@ describe('WorkflowAppLogList', () => {
|
||||
describe('Drawer', () => {
|
||||
it('should open drawer when clicking on a log row', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-123' }) })
|
||||
|
||||
const logs = createMockLogsResponse([
|
||||
createMockWorkflowLog({
|
||||
id: 'log-1',
|
||||
@@ -473,7 +471,7 @@ describe('WorkflowAppLogList', () => {
|
||||
const user = userEvent.setup()
|
||||
const appDetail = createMockApp()
|
||||
const onRefresh = vi.fn()
|
||||
useAppStore.setState({ appDetail })
|
||||
|
||||
const logs = createMockLogsResponse([
|
||||
createMockWorkflowLog({ id: 'log-1', created_at: 100 }),
|
||||
createMockWorkflowLog({ id: 'log-2', created_at: 200 }),
|
||||
@@ -508,7 +506,7 @@ describe('WorkflowAppLogList', () => {
|
||||
it('should close drawer and call onRefresh when closing', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onRefresh = vi.fn()
|
||||
useAppStore.setState({ appDetail: createMockApp() })
|
||||
|
||||
const logs = createMockLogsResponse([createMockWorkflowLog()])
|
||||
|
||||
render(<WorkflowAppLogList logs={logs} appDetail={createMockApp()} onRefresh={onRefresh} />)
|
||||
@@ -588,7 +586,7 @@ describe('WorkflowAppLogList', () => {
|
||||
describe('Replay Functionality', () => {
|
||||
it('should allow replay when triggered from app-run', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-replay' }) })
|
||||
|
||||
const logs = createMockLogsResponse([
|
||||
createMockWorkflowLog({
|
||||
workflow_run: createMockWorkflowRun({
|
||||
@@ -612,13 +610,13 @@ describe('WorkflowAppLogList', () => {
|
||||
await user.click(replayButton)
|
||||
|
||||
expect(mockRouterPush).toHaveBeenCalledWith(
|
||||
'/app/app-replay/workflow?replayRunId=run-to-replay',
|
||||
'/app/test-app-id/workflow?replayRunId=run-to-replay',
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow replay when triggered from debugging', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-debug' }) })
|
||||
|
||||
const logs = createMockLogsResponse([
|
||||
createMockWorkflowLog({
|
||||
workflow_run: createMockWorkflowRun({
|
||||
@@ -644,7 +642,7 @@ describe('WorkflowAppLogList', () => {
|
||||
|
||||
it('should not show replay for webhook triggers', async () => {
|
||||
const user = userEvent.setup()
|
||||
useAppStore.setState({ appDetail: createMockApp({ id: 'app-webhook' }) })
|
||||
|
||||
const logs = createMockLogsResponse([
|
||||
createMockWorkflowLog({
|
||||
workflow_run: createMockWorkflowRun({
|
||||
|
||||
@@ -3,25 +3,23 @@ import type { FC } from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { RiCloseLine, RiPlayLargeLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import Run from '@/app/components/workflow/run'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
|
||||
type ILogDetail = {
|
||||
appId: string
|
||||
runID: string
|
||||
onClose: () => void
|
||||
canReplay?: boolean
|
||||
}
|
||||
|
||||
const DetailPanel: FC<ILogDetail> = ({ runID, onClose, canReplay = false }) => {
|
||||
const DetailPanel: FC<ILogDetail> = ({ appId, runID, onClose, canReplay = false }) => {
|
||||
const { t } = useTranslation(['appLog', 'common'])
|
||||
const appDetail = useStore((state) => state.appDetail)
|
||||
const router = useRouter()
|
||||
|
||||
const handleReplay = () => {
|
||||
if (!appDetail?.id) return
|
||||
router.push(`/app/${appDetail.id}/workflow?replayRunId=${runID}`)
|
||||
router.push(`/app/${appId}/workflow?replayRunId=${runID}`)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -60,10 +58,8 @@ const DetailPanel: FC<ILogDetail> = ({ runID, onClose, canReplay = false }) => {
|
||||
</div>
|
||||
<WorkflowContextProvider>
|
||||
<Run
|
||||
runDetailUrl={runID ? `/apps/${appDetail?.id}/workflow-runs/${runID}` : ''}
|
||||
tracingListUrl={
|
||||
runID ? `/apps/${appDetail?.id}/workflow-runs/${runID}/node-executions` : ''
|
||||
}
|
||||
runDetailUrl={runID ? `/apps/${appId}/workflow-runs/${runID}` : ''}
|
||||
tracingListUrl={runID ? `/apps/${appId}/workflow-runs/${runID}/node-executions` : ''}
|
||||
/>
|
||||
</WorkflowContextProvider>
|
||||
</div>
|
||||
|
||||
@@ -290,6 +290,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
<DrawerPopup className="p-0! data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-3 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-full data-[swipe-direction=right]:max-w-150 data-[swipe-direction=right]:rounded-xl data-[swipe-direction=right]:border data-[swipe-direction=right]:border-components-panel-border">
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
<DetailPanel
|
||||
appId={appDetail.id}
|
||||
onClose={onCloseDrawer}
|
||||
runID={currentLog?.workflow_run.id || ''}
|
||||
canReplay={
|
||||
|
||||
Reference in new issue
Block a user