feat(scan-overview): add yaml configuration tab and improve logs layout

- Add yaml_configuration field to ScanHistorySerializer for backend exposure
- Implement tabbed interface with Logs and Configuration tabs in scan overview
- Add YamlEditor component to display scan configuration in read-only mode
- Refactor logs section to show status bar only when logs tab is active
- Move auto-refresh toggle to logs tab header for better UX
- Add padding to stage progress items for improved visual alignment
- Add internationalization strings for new UI elements (en and zh)
- Update ScanHistory type to include yamlConfiguration field
- Improve tab switching state management with activeTab state
This commit is contained in:
yyhuni
2026-01-08 07:31:54 +08:00
parent 8bb737a9fa
commit 5da7229873
5 changed files with 64 additions and 24 deletions

View File

@@ -41,7 +41,7 @@ class ScanHistorySerializer(serializers.ModelSerializer):
fields = [
'id', 'target', 'target_name', 'engine_ids', 'engine_names',
'worker_name', 'created_at', 'status', 'error_message', 'summary',
'progress', 'current_stage', 'stage_progress'
'progress', 'current_stage', 'stage_progress', 'yaml_configuration'
]
def get_summary(self, obj):

View File

@@ -31,9 +31,11 @@ import { Badge } from "@/components/ui/badge"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useScan } from "@/hooks/use-scans"
import { useScanLogs } from "@/hooks/use-scan-logs"
import { ScanLogList } from "@/components/scan/scan-log-list"
import { YamlEditor } from "@/components/ui/yaml-editor"
import { getDateLocale } from "@/lib/date-utils"
import { cn } from "@/lib/utils"
import type { StageStatus } from "@/types/scan.types"
@@ -96,6 +98,9 @@ export function ScanOverview({ scanId }: ScanOverviewProps) {
// Auto-refresh state (default: on when running)
const [autoRefresh, setAutoRefresh] = useState(true)
// Tab state for logs/config
const [activeTab, setActiveTab] = useState<'logs' | 'config'>('logs')
// Logs hook
const { logs, loading: logsLoading } = useScanLogs({
scanId,
@@ -329,7 +334,7 @@ export function ScanOverview({ scanId }: ScanOverviewProps) {
<div
key={stageName}
className={cn(
"flex items-center justify-between py-2 rounded-md transition-colors text-sm",
"flex items-center justify-between py-2 px-2 rounded-md transition-colors text-sm",
isRunning && "bg-[#d29922]/10 border border-[#d29922]/30",
stageProgress.status === "completed" && "text-muted-foreground",
stageProgress.status === "failed" && "bg-[#da3633]/10 text-[#da3633]",
@@ -400,28 +405,18 @@ export function ScanOverview({ scanId }: ScanOverviewProps) {
</Link>
</div>
{/* Right Column: Logs */}
{/* Right Column: Logs / Config */}
<div className="flex flex-col min-h-0 rounded-lg overflow-hidden border">
<div className="flex-1 min-h-0">
<ScanLogList logs={logs} loading={logsLoading} />
</div>
{/* Bottom status bar */}
<div className="flex items-center justify-between px-4 py-2 bg-muted/50 border-t text-xs text-muted-foreground shrink-0">
<div className="flex items-center gap-3">
<span>{t("logsTitle")}</span>
<Separator orientation="vertical" className="h-3" />
<span>{logs.length} </span>
{isRunning && autoRefresh && (
<>
<Separator orientation="vertical" className="h-3" />
<span className="flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-green-500 animate-pulse" />
3
</span>
</>
)}
</div>
{isRunning && (
{/* Tab Header */}
<div className="flex items-center justify-between px-3 py-2 bg-muted/30 border-b shrink-0">
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as 'logs' | 'config')}>
<TabsList variant="underline" className="h-8 gap-3">
<TabsTrigger variant="underline" value="logs" className="text-xs">{t("logsTitle")}</TabsTrigger>
<TabsTrigger variant="underline" value="config" className="text-xs">{t("configTitle")}</TabsTrigger>
</TabsList>
</Tabs>
{/* Auto-refresh toggle (only for logs tab when running) */}
{activeTab === 'logs' && isRunning && (
<div className="flex items-center gap-2">
<Switch
id="log-auto-refresh"
@@ -430,11 +425,49 @@ export function ScanOverview({ scanId }: ScanOverviewProps) {
className="scale-75"
/>
<Label htmlFor="log-auto-refresh" className="text-xs cursor-pointer">
{t("autoRefresh")}
</Label>
</div>
)}
</div>
{/* Tab Content */}
<div className="flex-1 min-h-0">
{activeTab === 'logs' ? (
<ScanLogList logs={logs} loading={logsLoading} />
) : (
<div className="h-full">
{scan.yamlConfiguration ? (
<YamlEditor
value={scan.yamlConfiguration}
onChange={() => {}}
disabled={true}
height="100%"
/>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
{t("noConfig")}
</div>
)}
</div>
)}
</div>
{/* Bottom status bar (only for logs tab) */}
{activeTab === 'logs' && (
<div className="flex items-center px-4 py-2 bg-muted/50 border-t text-xs text-muted-foreground shrink-0">
<span>{logs.length} </span>
{isRunning && autoRefresh && (
<>
<Separator orientation="vertical" className="h-3 mx-3" />
<span className="flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-green-500 animate-pulse" />
3
</span>
</>
)}
</div>
)}
</div>
</div>
</div>

View File

@@ -749,6 +749,9 @@
"stagesTitle": "Scan Progress",
"stagesCompleted": "completed",
"logsTitle": "Scan Logs",
"configTitle": "Configuration",
"autoRefresh": "Auto Refresh",
"noConfig": "No configuration available",
"noStages": "No stage progress available",
"totalFound": "total found",
"totalVulns": "{count} total",

View File

@@ -749,6 +749,9 @@
"stagesTitle": "扫描进度",
"stagesCompleted": "完成",
"logsTitle": "扫描日志",
"configTitle": "扫描配置",
"autoRefresh": "自动刷新",
"noConfig": "暂无配置信息",
"noStages": "暂无阶段进度",
"totalFound": "个漏洞",
"totalVulns": "共 {count} 个",

View File

@@ -59,6 +59,7 @@ export interface ScanRecord {
progress: number // 0-100
currentStage?: ScanStage // Current scan stage (only has value in running status)
stageProgress?: StageProgress // Stage progress details
yamlConfiguration?: string // YAML configuration string
}
export interface GetScansParams {