refactor(web): ErrorsPanel 抽取 — /observability 3 個 Tab 已無雙重 Layout
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
OG T
2026-04-09 10:51:59 +08:00
parent 22fa6ea413
commit 11fc2860cf
4 changed files with 66 additions and 154 deletions

View File

@@ -1,164 +1,16 @@
'use client'
/**
* Errors Page - #44 錯誤追蹤頁面
* ==============================
* Phase 10: Sentry + OpenClaw + UI 整合
*
* Nothing.tech 視覺規範:
* - 純白底色 (bg-white)
* - 極細淺灰邊框 (border border-gray-200)
* - 無圓角或微圓角 (rounded-sm)
* - 嚴禁陰影 (shadow-none)
*
* 佈局:
* - 左側: 統計卡片 + 趨勢圖
* - 右側: 問題列表
*
* i18n: 100% next-intl零硬編碼
*
* 建立: 2026-03-26 (台北時區)
* 建立者: Claude Code (#44 Error UI)
* Errors Page — Sprint 5: 內容抽取到 ErrorsPanel
*/
import { useTranslations } from 'next-intl'
import { AppLayout } from '@/components/layout'
import { useErrors } from '@/hooks/useErrors'
import { useUXAudit } from '@/hooks/useUXAudit'
import {
ErrorOverviewCard,
RecentIssuesList,
ErrorTrendChart,
UXAuditCard,
} from '@/components/errors'
import { Bug, RefreshCw } from 'lucide-react'
import type { SentryIssue } from '@/lib/api-client'
// =============================================================================
// Page Component
// =============================================================================
export default function ErrorsPage({
params,
}: {
params: { locale: string }
}) {
const t = useTranslations('errors')
const {
stats,
issues,
trends,
loading,
error,
activePeriod,
refetch,
setPeriod,
} = useErrors()
// #126: UX Audit / Session Replay 數據
const {
data: uxAuditData,
loading: uxAuditLoading,
error: uxAuditError,
} = useUXAudit()
const handleIssueClick = (issue: SentryIssue) => {
// Open in new tab if permalink available
if (issue.permalink) {
window.open(issue.permalink, '_blank', 'noopener,noreferrer')
}
}
import { ErrorsPanel } from '@/components/panels/ErrorsPanel'
export default function ErrorsPage({ params }: { params: { locale: string } }) {
return (
<AppLayout locale={params.locale}>
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Bug className="h-6 w-6 text-gray-700" />
<div>
<h1 className="text-xl font-semibold text-gray-900">
{t('title')}
</h1>
<p className="text-sm text-gray-500">
{t('subtitle')}
</p>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-3">
<button
onClick={refetch}
disabled={loading}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm
bg-gray-100 hover:bg-gray-200 rounded transition-colors
disabled:opacity-50 disabled:cursor-not-allowed"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
{loading ? t('loading') : t('refresh')}
</button>
{/*
Sentry Dashboard 連結已移除
原因: 內網 IP 會觸發瀏覽器「存取區域網路」權限對話框
2026-03-30 ogt: 參考 feedback_sentry_local_network.md
未來: 可透過 VPN 或反向代理公開 Sentry UI
*/}
</div>
</div>
{/* Error State */}
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-sm">
<p className="text-sm text-red-700">{error}</p>
</div>
)}
{/* Main Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Stats & Trends */}
<div className="lg:col-span-1 space-y-6">
{/* Overview Card */}
<ErrorOverviewCard
stats={stats}
loading={loading}
changePercent={trends?.change_percent}
/>
{/* Trend Chart */}
<ErrorTrendChart
data={trends}
loading={loading}
activePeriod={activePeriod}
onPeriodChange={setPeriod}
/>
{/* #126: UX Audit / Session Replay */}
<UXAuditCard
data={uxAuditData}
loading={uxAuditLoading}
error={uxAuditError}
/>
</div>
{/* Right Column - Issues List */}
<div className="lg:col-span-2">
<RecentIssuesList
issues={issues}
loading={loading}
onIssueClick={handleIssueClick}
/>
</div>
</div>
{/* Footer Info */}
<div className="mt-6 pt-4 border-t border-gray-100">
<p className="text-xs text-gray-400 text-center">
{t('footerInfo')}
</p>
</div>
</div>
<ErrorsPanel />
</AppLayout>
)
}

View File

@@ -20,9 +20,10 @@ import { AppLayout } from '@/components/layout'
import { PageTabs, type TabConfig } from '@/components/layout/page-tabs'
import { MonitoringPanel } from '@/components/panels/MonitoringPanel'
import { ApmPanel } from '@/components/panels/ApmPanel'
import { ErrorsPanel } from '@/components/panels/ErrorsPanel'
import { LobsterLoading } from '@/components/shared/lobster-loading'
// Tab 3-5: 暫時 lazy import (未來抽取 Panel)
// Tab 4-5: 暫時 lazy import (未來抽取 Panel)
const ErrorsContent = lazy(() => import('@/app/[locale]/errors/page'))
const AppsContent = lazy(() => import('@/app/[locale]/apps/page'))
const ServicesContent = lazy(() => import('@/app/[locale]/services/page'))
@@ -44,7 +45,7 @@ export default function ObservabilityPage({ params }: { params: { locale: string
{
id: 'errors',
label: t('errors'),
content: <Suspense fallback={<LobsterLoading />}><ErrorsContent params={params} /></Suspense>,
content: <ErrorsPanel />,
},
{
id: 'apps',

View File

@@ -0,0 +1,58 @@
'use client'
/**
* ErrorsPanel — 錯誤追蹤面板 (不含 AppLayout)
* Sprint 5: 從 /errors/page.tsx 抽取
*/
import { useTranslations } from 'next-intl'
import { useErrors } from '@/hooks/useErrors'
import { useUXAudit } from '@/hooks/useUXAudit'
import {
ErrorOverviewCard,
RecentIssuesList,
ErrorTrendChart,
UXAuditCard,
} from '@/components/errors'
import { Bug, RefreshCw } from 'lucide-react'
import type { SentryIssue } from '@/lib/api-client'
export function ErrorsPanel() {
const t = useTranslations('errors')
const { stats, issues, trends, loading, error, activePeriod, refetch, setPeriod } = useErrors()
const { data: uxAuditData, loading: uxAuditLoading, error: uxAuditError } = useUXAudit()
const handleIssueClick = (issue: SentryIssue) => {
if (issue.permalink) window.open(issue.permalink, '_blank', 'noopener,noreferrer')
}
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Bug className="h-6 w-6 text-gray-700" />
<div>
<h1 className="text-xl font-semibold text-gray-900">{t('title')}</h1>
<p className="text-sm text-gray-500">{t('subtitle')}</p>
</div>
</div>
<button onClick={refetch} disabled={loading} className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-gray-100 hover:bg-gray-200 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
{loading ? t('loading') : t('refresh')}
</button>
</div>
{error && <div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-sm"><p className="text-sm text-red-700">{error}</p></div>}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-1 space-y-6">
<ErrorOverviewCard stats={stats} loading={loading} changePercent={trends?.change_percent} />
<ErrorTrendChart data={trends} loading={loading} activePeriod={activePeriod} onPeriodChange={setPeriod} />
<UXAuditCard data={uxAuditData} loading={uxAuditLoading} error={uxAuditError} />
</div>
<div className="lg:col-span-2">
<RecentIssuesList issues={issues} loading={loading} onIssueClick={handleIssueClick} />
</div>
</div>
<div className="mt-6 pt-4 border-t border-gray-100"><p className="text-xs text-gray-400 text-center">{t('footerInfo')}</p></div>
</div>
)
}

View File

@@ -14,3 +14,4 @@
export { MonitoringPanel } from './MonitoringPanel'
export { ApmPanel } from './ApmPanel'
export { ErrorsPanel } from './ErrorsPanel'