dify/web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

208 lines
7.0 KiB
TypeScript

'use client'
import type { TriggerLogEntity } from '@/app/components/workflow/block-selector/types'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import {
RiArrowDownSLine,
RiArrowRightSLine,
RiCheckboxCircleFill,
RiErrorWarningFill,
RiFileCopyLine,
} from '@remixicon/react'
import dayjs from 'dayjs'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
type Props = Readonly<{
logs: TriggerLogEntity[]
className?: string
}>
enum LogTypeEnum {
REQUEST = 'request',
RESPONSE = 'response',
}
const LogViewer = ({ logs, className }: Props) => {
const { t } = useTranslation()
const [expandedLogs, setExpandedLogs] = useState<Set<string>>(new Set())
const toggleLogExpansion = (logId: string) => {
const newExpanded = new Set(expandedLogs)
if (newExpanded.has(logId)) newExpanded.delete(logId)
else newExpanded.add(logId)
setExpandedLogs(newExpanded)
}
const parseRequestData = (data: any) => {
if (typeof data === 'string' && data.startsWith('payload=')) {
try {
const urlDecoded = decodeURIComponent(data.substring(8)) // Remove 'payload='
return JSON.parse(urlDecoded)
} catch {
return data
}
}
if (typeof data === 'object') return data
try {
return JSON.parse(data)
} catch {
return data
}
}
const renderJsonContent = (originalData: any, title: LogTypeEnum) => {
const parsedData =
title === LogTypeEnum.REQUEST
? { headers: originalData.headers, data: parseRequestData(originalData.data) }
: originalData
const isJsonObject = typeof parsedData === 'object'
if (isJsonObject) {
return (
<CodeEditor
readOnly
title={<div className="system-xs-semibold-uppercase text-text-secondary">{title}</div>}
language={CodeLanguage.json}
value={parsedData}
isJSONStringifyBeauty
nodeId=""
/>
)
}
return (
<div className="rounded-md bg-components-input-bg-normal">
<div className="flex items-center justify-between px-2 py-1">
<div className="system-xs-semibold-uppercase text-text-secondary">{title}</div>
<button
onClick={(e) => {
e.stopPropagation()
navigator.clipboard.writeText(String(parsedData))
toast.success(t(($) => $['actionMsg.copySuccessfully'], { ns: 'common' }))
}}
className="rounded-md p-0.5 hover:bg-components-panel-border"
>
<RiFileCopyLine className="size-4 text-text-tertiary" />
</button>
</div>
<div className="px-2 pt-1 pb-2">
<pre className="code-xs-regular break-all whitespace-pre-wrap text-text-secondary">
{String(parsedData)}
</pre>
</div>
</div>
)
}
if (!logs || logs.length === 0) return null
return (
<div className={cn('flex flex-col gap-1', className)}>
{logs.map((log, index) => {
const logId = log.id || index.toString()
const isExpanded = expandedLogs.has(logId)
const isSuccess = log.response.status_code === 200
const isError = log.response.status_code >= 400
return (
<div
key={logId}
className={cn(
'relative overflow-hidden rounded-lg border bg-components-panel-on-panel-item-bg shadow-sm hover:bg-components-panel-on-panel-item-bg-hover',
isError && 'border-state-destructive-border',
!isError && isExpanded && 'border-components-panel-border',
!isError && !isExpanded && 'border-components-panel-border-subtle',
)}
>
{isError && (
<div className="pointer-events-none absolute top-0 left-0 h-7 w-[179px]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="179"
height="28"
viewBox="0 0 179 28"
fill="none"
className="size-full"
>
<g filter="url(#filter0_f_error_glow)">
<circle cx="27" cy="14" r="32" fill="#F04438" fillOpacity="0.25" />
</g>
<defs>
<filter
id="filter0_f_error_glow"
x="-125"
y="-138"
width="304"
height="304"
filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="BackgroundImageFix"
result="shape"
/>
<feGaussianBlur stdDeviation="60" result="effect1_foregroundBlur" />
</filter>
</defs>
</svg>
</div>
)}
<button
onClick={() => toggleLogExpansion(logId)}
className={cn(
'flex w-full items-center justify-between px-2 py-1.5 text-left',
isExpanded ? 'pt-2 pb-1' : 'min-h-7',
)}
>
<div className="flex items-center gap-0">
{isExpanded ? (
<RiArrowDownSLine className="size-4 text-text-tertiary" />
) : (
<RiArrowRightSLine className="size-4 text-text-tertiary" />
)}
<div className="system-xs-semibold-uppercase text-text-secondary">
{t(($) => $[`modal.manual.logs.${LogTypeEnum.REQUEST}`], { ns: 'pluginTrigger' })}{' '}
#{index + 1}
</div>
</div>
<div className="flex items-center gap-1">
<div className="system-xs-regular text-text-tertiary">
{dayjs(log.created_at).format('HH:mm:ss')}
</div>
<div className="size-3.5">
{isSuccess ? (
<RiCheckboxCircleFill className="size-full text-text-success" />
) : (
<RiErrorWarningFill className="size-full text-text-destructive" />
)}
</div>
</div>
</button>
{isExpanded && (
<div className="flex flex-col gap-1 px-1 pb-1">
{renderJsonContent(log.request, LogTypeEnum.REQUEST)}
{renderJsonContent(log.response, LogTypeEnum.RESPONSE)}
</div>
)}
</div>
)
})}
</div>
)
}
export default LogViewer