refactor(logs): scope page data and replay to the owning app (#43260)

This commit is contained in:
yyh authored and GitHub committed 2026-09-30 04:35:55 +00:00
1 parent 0effc36f45
commit d5c3fb9ecd
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={