From 665481811a23508c88a2f217a0137adb2ef23f88 Mon Sep 17 00:00:00 2001 From: zhaohaihzb Date: Wed, 29 Jul 2026 14:40:12 +0800 Subject: [PATCH 1/2] [studio] Implement format utility functions (date, bytes, number, delay, percent) Replace TODO stub implementations in format.ts with full-featured utilities: - formatDateTime / formatDate: proper date formatting with padding - formatBytes: human-readable 1024-based byte formatting - formatNumber: thousands separator formatting - formatDelay: duration formatting with i18n support (zh/en) - formatPercent: fixed-decimal percentage formatting --- web/src/utils/format.ts | 85 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/web/src/utils/format.ts b/web/src/utils/format.ts index c3db0e94f..4df61936c 100644 --- a/web/src/utils/format.ts +++ b/web/src/utils/format.ts @@ -15,12 +15,87 @@ * limitations under the License. */ +const pad = (n: number, width = 2): string => String(n).padStart(width, '0'); + +/** + * Format a date string or Date object to 'YYYY-MM-DD HH:mm:ss'. + */ +export function formatDateTime(date: string | Date): string { + const d = typeof date === 'string' ? new Date(date) : date; + if (isNaN(d.getTime())) return String(date); + return ( + `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` + + `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` + ); +} + +/** + * Format a date string or Date object to 'YYYY-MM-DD'. + */ export function formatDate(date: string | Date): string { - // TODO: implement - return String(date); + const d = typeof date === 'string' ? new Date(date) : date; + if (isNaN(d.getTime())) return String(date); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; } -export function formatBytes(bytes: number): string { - // TODO: implement - return `${bytes} B`; +/** + * Format bytes into human-readable string (1024-based). + * e.g. 1536 → '1.5 KB', 1048576 → '1 MB' + */ +export function formatBytes(bytes: number, decimals = 1): string { + if (bytes === 0) return '0 B'; + if (bytes < 0) return `-${formatBytes(-bytes, decimals)}`; + + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const k = 1024; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + const value = bytes / Math.pow(k, i); + return `${value.toFixed(decimals)} ${units[i]}`; +} + +/** + * Format a number with thousands separators. + * e.g. 1234567 → '1,234,567' + */ +export function formatNumber(num: number): string { + return num.toLocaleString('en-US'); +} + +/** + * Format delay seconds into human-readable duration. + * Supports i18n via the lang parameter. + * e.g. 82500 → zh: "22小时55分钟", en: "22h 55m" + */ +export function formatDelay(totalSeconds: number, lang: 'zh' | 'en' = 'zh'): string { + if (totalSeconds <= 0) return lang === 'zh' ? '0秒' : '0s'; + + const days = Math.floor(totalSeconds / 86400); + let remaining = totalSeconds % 86400; + const hours = Math.floor(remaining / 3600); + remaining %= 3600; + const minutes = Math.floor(remaining / 60); + const seconds = remaining % 60; + + if (lang === 'en') { + const parts: string[] = []; + if (days > 0) parts.push(`${days}d`); + if (hours > 0) parts.push(`${hours}h`); + if (minutes > 0) parts.push(`${minutes}m`); + if (seconds > 0 && parts.length < 3) parts.push(`${seconds}s`); + return parts.length > 0 ? parts.join(' ') : '0s'; + } + + const parts: string[] = []; + if (days > 0) parts.push(`${days}天`); + if (hours > 0) parts.push(`${hours}小时`); + if (minutes > 0) parts.push(`${minutes}分钟`); + if (seconds > 0 && parts.length < 3) parts.push(`${seconds}秒`); + return parts.length > 0 ? parts.join('') : '0秒'; +} + +/** + * Format a percentage value (0-100) with fixed decimals. + */ +export function formatPercent(value: number, decimals = 1): string { + return `${value.toFixed(decimals)}%`; } From 8f14dd174b0a465e30aadb93aa90ac342960c080 Mon Sep 17 00:00:00 2001 From: zhaohaihzb Date: Wed, 29 Jul 2026 14:41:07 +0800 Subject: [PATCH 2/2] [studio] Enhance MiniLine chart component Smooth cardinal-spline path, gradient area fill, glow on last-point dot, configurable strokeWidth/showDot/animated, and responsive SVG mode. The MiniBar zero-value change from the original PR is dropped: trunk intentionally renders no visible bar at zero throughput (#629). --- web/src/components/MiniLine.tsx | 127 ++++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 23 deletions(-) diff --git a/web/src/components/MiniLine.tsx b/web/src/components/MiniLine.tsx index 8a483ba84..a7658d247 100644 --- a/web/src/components/MiniLine.tsx +++ b/web/src/components/MiniLine.tsx @@ -15,12 +15,25 @@ * limitations under the License. */ +import { useMemo } from 'react'; + +let _lineId = 0; + interface MiniLineProps { data: number[]; color?: string; height?: number; width?: number; + /** Fill area under the curve */ fill?: boolean; + /** Stroke width */ + strokeWidth?: number; + /** Show dot on last data point */ + showDot?: boolean; + /** Animate on mount */ + animated?: boolean; + /** Make SVG responsive (width=100%, preserves aspect ratio) */ + responsive?: boolean; } const MiniLine = ({ @@ -29,44 +42,112 @@ const MiniLine = ({ height = 32, width = 120, fill = true, + strokeWidth = 2, + showDot = true, + animated = true, + responsive = false, }: MiniLineProps) => { - if (data.length < 2) return null; - const max = Math.max(...data, 1); const min = Math.min(...data, 0); const range = max - min || 1; - const padding = 2; - const innerW = width - padding * 2; - const innerH = height - padding * 2; + const pad = 4; + const innerW = width - pad * 2; + const innerH = height - pad * 2; + + const gradientId = useMemo(() => `ml-grad-${++_lineId}`, []); + const glowId = useMemo(() => `ml-glow-${++_lineId}`, []); - const points = data.map((v, i) => { - const x = padding + (i / (data.length - 1)) * innerW; - const y = padding + innerH - ((v - min) / range) * innerH; - return `${x},${y}`; - }); + if (data.length < 2) return null; + + // Build smooth Catmull-Rom → Bezier control points + const points = data.map((v, i) => ({ + x: pad + (i / (data.length - 1)) * innerW, + y: pad + innerH - ((v - min) / range) * innerH, + })); + + // Convert points to a smooth SVG path using cardinal spline + const smoothPath = (() => { + if (points.length < 2) return ''; + let d = `M${points[0].x},${points[0].y}`; + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[Math.max(0, i - 1)]; + const p1 = points[i]; + const p2 = points[i + 1]; + const p3 = points[Math.min(points.length - 1, i + 2)]; + const tension = 0.3; + const cp1x = p1.x + (p2.x - p0.x) * tension; + const cp1y = p1.y + (p2.y - p0.y) * tension; + const cp2x = p2.x - (p3.x - p1.x) * tension; + const cp2y = p2.y - (p3.y - p1.y) * tension; + d += ` C${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`; + } + return d; + })(); - const linePath = `M${points.join(' L')}`; - const areaPath = `${linePath} L${padding + innerW},${height - padding} L${padding},${height - padding} Z`; + const areaPath = `${smoothPath} L${pad + innerW},${height - pad} L${pad},${height - pad} Z`; + + const lastPoint = points[points.length - 1]; return ( - - {fill && } + + {' '} + + + + + + + + + + + + + + {fill && } - {/* Last point dot */} - + {showDot && ( + <> + + + + )} + ); };