diff --git a/apps/web/src/app/[locale]/apm/page.tsx b/apps/web/src/app/[locale]/apm/page.tsx index 7d230e00..ac483d87 100644 --- a/apps/web/src/app/[locale]/apm/page.tsx +++ b/apps/web/src/app/[locale]/apm/page.tsx @@ -1,128 +1,16 @@ 'use client' /** - * APM Page — 黃金指標 (Golden Signals) - * @created 2026-04-01 ogt - 路由佔位 - * @updated 2026-04-03 Claude Code - 串接 /api/v1/metrics/gold 真實數據 + TimeSeriesChart 趨勢圖 + * APM Page — Sprint 5: 內容抽取到 ApmPanel */ -import { useState, useEffect } from 'react' -import { useTranslations } from 'next-intl' import { AppLayout } from '@/components/layout' -import { TimeSeriesChart } from '@/components/charts/time-series-chart' - -const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' -const SIGNOZ_URL = 'http://192.168.0.188:3301' - -interface GoldMetricItem { - label: string - value: number | string - unit: string | null - trend: number[] - status: string -} - -interface GoldMetricsResponse { - timestamp: string - service_name: string - metrics: GoldMetricItem[] -} - -const STATUS_COLOR: Record = { - healthy: '#22C55E', - warning: '#F59E0B', - critical: '#cc2200', - unknown: '#87867f', -} - -const STATUS_CHART_COLOR: Record = { - healthy: 'success', - warning: 'warning', - critical: 'error', - unknown: 'primary', -} +import { ApmPanel } from '@/components/panels/ApmPanel' export default function ApmPage({ params }: { params: { locale: string } }) { - const t = useTranslations('apm') - const [data, setData] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - fetch(`${API_BASE}/api/v1/metrics/gold?service_name=awoooi-api&time_window_minutes=10`) - .then(r => r.json()) - .then((d: GoldMetricsResponse) => { setData(d); setLoading(false) }) - .catch(err => { setError(String(err)); setLoading(false) }) - }, []) - return ( -
-
-
-

{t('title')}

-

{t('subtitle')}

-
- - ↗ {t('openSignoz')} - -
- {loading ? ( -
{t('loading')}
- ) : error ? ( -
{t('error')}
- ) : data && data.metrics.length > 0 ? ( - <> - {/* Metric Cards with Sparklines */} -
- {data.metrics.map((m, i) => { - const trendPoints = (m.trend ?? []).map((v, idx) => ({ timestamp: idx, value: v })) - const hasTrend = trendPoints.length > 1 - const chartColor = STATUS_CHART_COLOR[m.status] ?? 'primary' - return ( -
-
{m.label}
-
- {typeof m.value === 'number' ? m.value.toFixed(2) : m.value} - {m.unit && {m.unit}} -
-
- - {m.status} -
- {hasTrend && ( - - )} -
- ) - })} -
-
- - Service: {data.service_name} - {' · '} - {new Date(data.timestamp).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' })} - -
- - ) : ( -
-
-
-
{t('noData')}
-
{t('noDataDescription')}
-
-
- )} -
+
) } diff --git a/apps/web/src/app/[locale]/observability/page.tsx b/apps/web/src/app/[locale]/observability/page.tsx index 005b53c7..59e40119 100644 --- a/apps/web/src/app/[locale]/observability/page.tsx +++ b/apps/web/src/app/[locale]/observability/page.tsx @@ -19,10 +19,10 @@ import { useTranslations } from 'next-intl' 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 { LobsterLoading } from '@/components/shared/lobster-loading' -// Tab 2-5: 暫時 lazy import 原始頁面 (含 AppLayout,未來抽取) -const APMContent = lazy(() => import('@/app/[locale]/apm/page')) +// Tab 3-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')) @@ -39,7 +39,7 @@ export default function ObservabilityPage({ params }: { params: { locale: string { id: 'apm', label: t('apm'), - content: }>, + content: , }, { id: 'errors', diff --git a/apps/web/src/components/panels/ApmPanel.tsx b/apps/web/src/components/panels/ApmPanel.tsx new file mode 100644 index 00000000..4aa59d92 --- /dev/null +++ b/apps/web/src/components/panels/ApmPanel.tsx @@ -0,0 +1,91 @@ +'use client' + +/** + * ApmPanel — APM 黃金指標面板 (不含 AppLayout) + * Sprint 5: 從 /apm/page.tsx 抽取 + */ + +import { useState, useEffect } from 'react' +import { useTranslations } from 'next-intl' +import { TimeSeriesChart } from '@/components/charts/time-series-chart' + +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' +const SIGNOZ_URL = 'http://192.168.0.188:3301' + +interface GoldMetricItem { + label: string; value: number | string; unit: string | null; trend: number[]; status: string +} +interface GoldMetricsResponse { + timestamp: string; service_name: string; metrics: GoldMetricItem[] +} + +const STATUS_COLOR: Record = { healthy: '#22C55E', warning: '#F59E0B', critical: '#cc2200', unknown: '#87867f' } +const STATUS_CHART_COLOR: Record = { healthy: 'success', warning: 'warning', critical: 'error', unknown: 'primary' } + +export function ApmPanel() { + const t = useTranslations('apm') + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + fetch(`${API_BASE}/api/v1/metrics/gold?service_name=awoooi-api&time_window_minutes=10`) + .then(r => r.json()) + .then((d: GoldMetricsResponse) => { setData(d); setLoading(false) }) + .catch(err => { setError(String(err)); setLoading(false) }) + }, []) + + return ( +
+
+
+

{t('title')}

+

{t('subtitle')}

+
+ + ↗ {t('openSignoz')} + +
+ {loading ? ( +
{t('loading')}
+ ) : error ? ( +
{t('error')}
+ ) : data && data.metrics.length > 0 ? ( + <> +
+ {data.metrics.map((m, i) => { + const trendPoints = (m.trend ?? []).map((v, idx) => ({ timestamp: idx, value: v })) + const hasTrend = trendPoints.length > 1 + const chartColor = STATUS_CHART_COLOR[m.status] ?? 'primary' + return ( +
+
{m.label}
+
+ {typeof m.value === 'number' ? m.value.toFixed(2) : m.value} + {m.unit && {m.unit}} +
+
+ + {m.status} +
+ {hasTrend && } +
+ ) + })} +
+
+ + Service: {data.service_name}{' · '}{new Date(data.timestamp).toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' })} + +
+ + ) : ( +
+
+
{t('noData')}
+
{t('noDataDescription')}
+
+ )} +
+ ) +} diff --git a/apps/web/src/components/panels/index.ts b/apps/web/src/components/panels/index.ts index 148e00d0..6e0e43cb 100644 --- a/apps/web/src/components/panels/index.ts +++ b/apps/web/src/components/panels/index.ts @@ -13,3 +13,4 @@ */ export { MonitoringPanel } from './MonitoringPanel' +export { ApmPanel } from './ApmPanel'