refactor(web): ErrorsPanel 抽取 — /observability 3 個 Tab 已無雙重 Layout
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
58
apps/web/src/components/panels/ErrorsPanel.tsx
Normal file
58
apps/web/src/components/panels/ErrorsPanel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -14,3 +14,4 @@
|
||||
|
||||
export { MonitoringPanel } from './MonitoringPanel'
|
||||
export { ApmPanel } from './ApmPanel'
|
||||
export { ErrorsPanel } from './ErrorsPanel'
|
||||
|
||||
Reference in New Issue
Block a user