refactor: extract InspectLayout composition component to eliminate repeated header/close patterns

Consolidate duplicated TabHeader + close button layout (8 occurrences) into a single
InspectLayout wrapper. Replace boolean props with children slots for better composition.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
yyh 2026-01-28 12:50:04 +08:00
parent d10d3b7021
commit ef6f7f2a6c
No known key found for this signature in database
5 changed files with 128 additions and 66 deletions

View File

@ -157,24 +157,22 @@ const ArtifactsTab: FC = () => {
)}
<div
className={cn(
'w-60 shrink-0 border-r border-divider-burn',
'flex w-60 shrink-0 flex-col border-r border-divider-burn',
bottomPanelWidth < 488
? showLeftPanel
? 'absolute left-0 top-0 z-10 h-full w-[217px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-sm'
: 'hidden'
: 'block',
: '',
)}
>
<div className="flex h-full flex-col">
<div className="grow overflow-y-auto py-1">
<ArtifactsTree
data={treeData}
onDownload={handleDownload}
onSelect={handleFileSelect}
selectedPath={selectedFile?.path}
isDownloading={downloadMutation.isPending}
/>
</div>
<div className="min-h-0 flex-1 overflow-y-auto py-1">
<ArtifactsTree
data={treeData}
onDownload={handleDownload}
onSelect={handleFileSelect}
selectedPath={selectedFile?.path}
isDownloading={downloadMutation.isPending}
/>
</div>
</div>
<div className="w-0 grow">

View File

@ -0,0 +1,41 @@
import type { FC, ReactNode } from 'react'
import type { InspectTab } from './types'
import { RiCloseLine } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import TabHeader from './tab-header'
type InspectLayoutProps = {
activeTab: InspectTab
onTabChange: (tab: InspectTab) => void
onClose: () => void
headerActions?: ReactNode
children: ReactNode
}
const InspectLayout: FC<InspectLayoutProps> = ({
activeTab,
onTabChange,
onClose,
headerActions,
children,
}) => {
return (
<div className="flex h-full flex-col">
<div className="flex shrink-0 items-center justify-between">
<TabHeader activeTab={activeTab} onTabChange={onTabChange}>
{headerActions}
</TabHeader>
<div className="pr-2 pt-2">
<ActionButton onClick={onClose} aria-label="Close">
<RiCloseLine className="h-4 w-4" />
</ActionButton>
</div>
</div>
<div className="min-h-0 flex-1">
{children}
</div>
</div>
)
}
export default InspectLayout

View File

@ -1,25 +1,16 @@
import type { FC } from 'react'
import {
RiCloseLine,
} from '@remixicon/react'
import { lazy, Suspense, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import { cn } from '@/utils/classnames'
import useCurrentVars from '../hooks/use-inspect-vars-crud'
import { useStore } from '../store'
import InspectLayout from './inspect-layout'
import { InspectTab } from './types'
import VariablesTab from './variables-tab'
const ArtifactsTab = lazy(() => import('./artifacts-tab'))
const TAB_ITEMS = [
{ value: InspectTab.Variables, labelKey: 'debug.variableInspect.tab.variables' },
{ value: InspectTab.Artifacts, labelKey: 'debug.variableInspect.tab.artifacts' },
] as const
const Panel: FC = () => {
const { t } = useTranslation('workflow')
const setShowVariableInspectPanel = useStore(s => s.setShowVariableInspectPanel)
@ -42,44 +33,28 @@ const Panel: FC = () => {
setShowVariableInspectPanel(false)
}, [setShowVariableInspectPanel])
const headerActions = activeTab === InspectTab.Variables && !isVariablesEmpty
? (
<Button variant="ghost" size="small" onClick={handleClear}>
{t('debug.variableInspect.clearAll')}
</Button>
)
: undefined
return (
<div className={cn('flex h-full flex-col')}>
<div className="flex shrink-0 items-center justify-between gap-1 pl-3 pr-2 pt-2">
<div className="flex items-center gap-0.5">
{TAB_ITEMS.map(tab => (
<button
key={tab.value}
type="button"
onClick={() => setActiveTab(tab.value)}
className={cn(
'system-sm-semibold rounded-md px-2 py-1 transition-colors',
activeTab === tab.value
? 'bg-state-base-active text-text-primary'
: 'text-text-tertiary hover:text-text-secondary',
)}
>
{t(tab.labelKey)}
</button>
))}
{activeTab === InspectTab.Variables && !isVariablesEmpty && (
<Button variant="ghost" size="small" onClick={handleClear}>
{t('debug.variableInspect.clearAll')}
</Button>
)}
</div>
<ActionButton onClick={handleClose}>
<RiCloseLine className="h-4 w-4" />
</ActionButton>
</div>
<div className="min-h-0 flex-1">
{activeTab === InspectTab.Variables && <VariablesTab />}
{activeTab === InspectTab.Artifacts && (
<Suspense fallback={<div className="flex h-full items-center justify-center"><Loading /></div>}>
<ArtifactsTab />
</Suspense>
)}
</div>
</div>
<InspectLayout
activeTab={activeTab}
onTabChange={setActiveTab}
onClose={handleClose}
headerActions={headerActions}
>
{activeTab === InspectTab.Variables && <VariablesTab />}
{activeTab === InspectTab.Artifacts && (
<Suspense fallback={<div className="flex h-full items-center justify-center"><Loading /></div>}>
<ArtifactsTab />
</Suspense>
)}
</InspectLayout>
)
}

View File

@ -0,0 +1,46 @@
import type { FC, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import { InspectTab } from './types'
const TAB_ITEMS = [
{ value: InspectTab.Variables, labelKey: 'debug.variableInspect.tab.variables' },
{ value: InspectTab.Artifacts, labelKey: 'debug.variableInspect.tab.artifacts' },
] as const
type TabHeaderProps = {
activeTab: InspectTab
onTabChange: (tab: InspectTab) => void
children?: ReactNode
}
const TabHeader: FC<TabHeaderProps> = ({
activeTab,
onTabChange,
children,
}) => {
const { t } = useTranslation('workflow')
return (
<div className="flex shrink-0 items-center gap-0.5 pl-3 pr-2 pt-2">
{TAB_ITEMS.map(tab => (
<button
key={tab.value}
type="button"
onClick={() => onTabChange(tab.value)}
className={cn(
'system-sm-semibold rounded-md px-2 py-1 transition-colors',
activeTab === tab.value
? 'bg-state-base-active text-text-primary'
: 'text-text-tertiary hover:text-text-secondary',
)}
>
{t(tab.labelKey)}
</button>
))}
{children}
</div>
)
}
export default TabHeader

View File

@ -143,7 +143,7 @@ const VariablesTab: FC = () => {
if (isListening) {
return (
<div className="grow p-2">
<div className="h-full p-2">
<Listening onStop={handleStopListening} />
</div>
)
@ -162,18 +162,20 @@ const VariablesTab: FC = () => {
{bottomPanelWidth < 488 && showLeftPanel && <div role="presentation" className="absolute left-0 top-0 h-full w-full" onClick={() => setShowLeftPanel(false)}></div>}
<div
className={cn(
'w-60 shrink-0 border-r border-divider-burn',
'flex w-60 shrink-0 flex-col border-r border-divider-burn',
bottomPanelWidth < 488
? showLeftPanel
? 'absolute left-0 top-0 z-10 h-full w-[217px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-sm'
: 'hidden'
: 'block',
: '',
)}
>
<Left
currentNodeVar={currentNodeInfo as currentVarType}
handleVarSelect={handleNodeVarSelect}
/>
<div className="min-h-0 flex-1">
<Left
currentNodeVar={currentNodeInfo as currentVarType}
handleVarSelect={handleNodeVarSelect}
/>
</div>
</div>
<div className="w-0 grow">
<Right