refactor: split deployment info into server/client components

Refactor to use server component composition pattern where DeploymentInfoContent (server component) imports build info JSON directly and is rendered as a child of DeploymentInfoModal (client component). Eliminates unnecessary API fetch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-10-05 06:43:34 -05:00
parent 43be7ac83a
commit 5e7b273b33
3 changed files with 317 additions and 336 deletions

View File

@ -1,341 +1,10 @@
'use client'
import React, { useState, useEffect } from 'react'
import * as Dialog from '@radix-ui/react-dialog'
import { Info, X, GitBranch, GitCommit, Clock, Package, Server } from 'lucide-react'
import { css } from '../../styled-system/css'
import { vstack, hstack } from '../../styled-system/patterns'
interface BuildInfo {
version: string
buildTime: string
buildTimestamp: number
git: {
commit: string | null
commitShort: string | null
branch: string | null
tag: string | null
isDirty: boolean
}
environment: string
buildNumber: string | null
nodeVersion: string
}
import { DeploymentInfoModal } from './DeploymentInfoModal'
import { DeploymentInfoContent } from './DeploymentInfoContent'
export function DeploymentInfo() {
const [open, setOpen] = useState(false)
const [buildInfo, setBuildInfo] = useState<BuildInfo | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
// Keyboard shortcut: Cmd/Ctrl + Shift + I
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'I') {
e.preventDefault()
setOpen(prev => !prev)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
useEffect(() => {
if (open && !buildInfo) {
fetch('/api/build-info')
.then(res => res.json())
.then(data => {
setBuildInfo(data)
setLoading(false)
})
.catch(err => {
console.error('Failed to load build info:', err)
setLoading(false)
})
}
}, [open, buildInfo])
const formatTimestamp = (timestamp: number) => {
const date = new Date(timestamp)
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
})
}
const getTimeAgo = (timestamp: number) => {
const seconds = Math.floor((Date.now() - timestamp) / 1000)
const intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
}
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
const interval = Math.floor(seconds / secondsInUnit)
if (interval >= 1) {
return `${interval} ${unit}${interval === 1 ? '' : 's'} ago`
}
}
return 'just now'
}
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Portal>
<Dialog.Overlay className={css({
position: 'fixed',
inset: '0',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 9998,
animation: 'fadeIn 150ms cubic-bezier(0.16, 1, 0.3, 1)',
})} />
<Dialog.Content className={css({
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
borderRadius: 'lg',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
padding: '6',
width: '90vw',
maxWidth: '500px',
maxHeight: '85vh',
overflow: 'auto',
zIndex: 9999,
animation: 'contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)',
})}>
<Dialog.Title className={css({
fontSize: 'xl',
fontWeight: 'semibold',
marginBottom: '4',
display: 'flex',
alignItems: 'center',
gap: '2'
})}>
<Info size={24} />
Deployment Information
</Dialog.Title>
{loading ? (
<div className={css({ textAlign: 'center', padding: '8' })}>
Loading...
</div>
) : buildInfo ? (
<div className={vstack({ alignItems: 'stretch', gap: '4' })}>
<InfoRow
icon={<Package size={18} />}
label="Version"
value={buildInfo.version}
/>
<InfoRow
icon={<Clock size={18} />}
label="Build Time"
value={
<div className={vstack({ alignItems: 'flex-start', gap: '1' })}>
<span>{formatTimestamp(buildInfo.buildTimestamp)}</span>
<span className={css({ fontSize: 'sm', color: 'gray.600' })}>
{getTimeAgo(buildInfo.buildTimestamp)}
</span>
</div>
}
/>
{buildInfo.git.branch && (
<InfoRow
icon={<GitBranch size={18} />}
label="Branch"
value={
<span className={css({
fontFamily: 'mono',
fontSize: 'sm',
backgroundColor: 'gray.100',
padding: '1 2',
borderRadius: 'sm'
})}>
{buildInfo.git.branch}
{buildInfo.git.isDirty && (
<span className={css({ color: 'orange.600', marginLeft: '2' })}>
(dirty)
</span>
)}
</span>
}
/>
)}
{buildInfo.git.commitShort && (
<InfoRow
icon={<GitCommit size={18} />}
label="Commit"
value={
<div className={vstack({ alignItems: 'flex-start', gap: '1' })}>
<span className={css({
fontFamily: 'mono',
fontSize: 'sm',
backgroundColor: 'gray.100',
padding: '1 2',
borderRadius: 'sm'
})}>
{buildInfo.git.commitShort}
</span>
{buildInfo.git.commit && (
<span className={css({
fontFamily: 'mono',
fontSize: 'xs',
color: 'gray.500'
})}>
{buildInfo.git.commit}
</span>
)}
</div>
}
/>
)}
{buildInfo.git.tag && (
<InfoRow
icon={<Package size={18} />}
label="Tag"
value={
<span className={css({
fontFamily: 'mono',
fontSize: 'sm',
backgroundColor: 'blue.100',
color: 'blue.700',
padding: '1 2',
borderRadius: 'sm'
})}>
{buildInfo.git.tag}
</span>
}
/>
)}
<InfoRow
icon={<Server size={18} />}
label="Environment"
value={
<span className={css({
fontFamily: 'mono',
fontSize: 'sm',
backgroundColor: buildInfo.environment === 'production' ? 'green.100' : 'yellow.100',
color: buildInfo.environment === 'production' ? 'green.700' : 'yellow.700',
padding: '1 2',
borderRadius: 'sm'
})}>
{buildInfo.environment}
</span>
}
/>
{buildInfo.buildNumber && (
<InfoRow
label="Build Number"
value={buildInfo.buildNumber}
/>
)}
<InfoRow
label="Node Version"
value={
<span className={css({ fontFamily: 'mono', fontSize: 'sm' })}>
{buildInfo.nodeVersion}
</span>
}
/>
</div>
) : (
<div className={css({ textAlign: 'center', padding: '8', color: 'gray.600' })}>
Failed to load build information
</div>
)}
<div className={css({
marginTop: '6',
paddingTop: '4',
borderTop: '1px solid',
borderColor: 'gray.200',
fontSize: 'sm',
color: 'gray.600',
textAlign: 'center'
})}>
Press <kbd className={css({
fontFamily: 'mono',
backgroundColor: 'gray.100',
padding: '0.5 1.5',
borderRadius: 'sm',
border: '1px solid',
borderColor: 'gray.300'
})}>I</kbd> or <kbd className={css({
fontFamily: 'mono',
backgroundColor: 'gray.100',
padding: '0.5 1.5',
borderRadius: 'sm',
border: '1px solid',
borderColor: 'gray.300'
})}>CtrlI</kbd> to toggle
</div>
<Dialog.Close className={css({
position: 'absolute',
top: '4',
right: '4',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'md',
padding: '2',
color: 'gray.700',
cursor: 'pointer',
_hover: {
backgroundColor: 'gray.100'
}
})}>
<X size={20} />
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
function InfoRow({
icon,
label,
value
}: {
icon?: React.ReactNode
label: string
value: React.ReactNode
}) {
return (
<div className={hstack({
justifyContent: 'space-between',
gap: '4',
paddingY: '2',
borderBottom: '1px solid',
borderColor: 'gray.100'
})}>
<div className={hstack({ gap: '2', color: 'gray.700' })}>
{icon}
<span className={css({ fontWeight: 'medium' })}>{label}</span>
</div>
<div className={css({ textAlign: 'right', flex: '1' })}>
{value}
</div>
</div>
<DeploymentInfoModal>
<DeploymentInfoContent />
</DeploymentInfoModal>
)
}

View File

@ -0,0 +1,197 @@
import React from 'react'
import { GitBranch, GitCommit, Clock, Package, Server } from 'lucide-react'
import { css } from '../../styled-system/css'
import { vstack, hstack } from '../../styled-system/patterns'
import buildInfo from '@/generated/build-info.json'
function formatTimestamp(timestamp: number) {
const date = new Date(timestamp)
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
})
}
function getTimeAgo(timestamp: number) {
const seconds = Math.floor((Date.now() - timestamp) / 1000)
const intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1
}
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
const interval = Math.floor(seconds / secondsInUnit)
if (interval >= 1) {
return `${interval} ${unit}${interval === 1 ? '' : 's'} ago`
}
}
return 'just now'
}
export function DeploymentInfoContent() {
return (
<div className={vstack({ alignItems: 'stretch', gap: '4' })}>
<InfoRow
icon={<Package size={18} />}
label="Version"
value={buildInfo.version}
/>
<InfoRow
icon={<Clock size={18} />}
label="Build Time"
value={
<div className={vstack({ alignItems: 'flex-start', gap: '1' })}>
<span>{formatTimestamp(buildInfo.buildTimestamp)}</span>
<span className={css({ fontSize: 'sm', color: 'gray.600' })}>
{getTimeAgo(buildInfo.buildTimestamp)}
</span>
</div>
}
/>
{buildInfo.git.branch && (
<InfoRow
icon={<GitBranch size={18} />}
label="Branch"
value={
<span className={css({
fontFamily: 'mono',
fontSize: 'sm',
backgroundColor: 'gray.100',
padding: '1 2',
borderRadius: 'sm'
})}>
{buildInfo.git.branch}
{buildInfo.git.isDirty && (
<span className={css({ color: 'orange.600', marginLeft: '2' })}>
(dirty)
</span>
)}
</span>
}
/>
)}
{buildInfo.git.commitShort && (
<InfoRow
icon={<GitCommit size={18} />}
label="Commit"
value={
<div className={vstack({ alignItems: 'flex-start', gap: '1' })}>
<span className={css({
fontFamily: 'mono',
fontSize: 'sm',
backgroundColor: 'gray.100',
padding: '1 2',
borderRadius: 'sm'
})}>
{buildInfo.git.commitShort}
</span>
{buildInfo.git.commit && (
<span className={css({
fontFamily: 'mono',
fontSize: 'xs',
color: 'gray.500'
})}>
{buildInfo.git.commit}
</span>
)}
</div>
}
/>
)}
{buildInfo.git.tag && (
<InfoRow
icon={<Package size={18} />}
label="Tag"
value={
<span className={css({
fontFamily: 'mono',
fontSize: 'sm',
backgroundColor: 'blue.100',
color: 'blue.700',
padding: '1 2',
borderRadius: 'sm'
})}>
{buildInfo.git.tag}
</span>
}
/>
)}
<InfoRow
icon={<Server size={18} />}
label="Environment"
value={
<span className={css({
fontFamily: 'mono',
fontSize: 'sm',
backgroundColor: buildInfo.environment === 'production' ? 'green.100' : 'yellow.100',
color: buildInfo.environment === 'production' ? 'green.700' : 'yellow.700',
padding: '1 2',
borderRadius: 'sm'
})}>
{buildInfo.environment}
</span>
}
/>
{buildInfo.buildNumber && (
<InfoRow
label="Build Number"
value={buildInfo.buildNumber}
/>
)}
<InfoRow
label="Node Version"
value={
<span className={css({ fontFamily: 'mono', fontSize: 'sm' })}>
{buildInfo.nodeVersion}
</span>
}
/>
</div>
)
}
function InfoRow({
icon,
label,
value
}: {
icon?: React.ReactNode
label: string
value: React.ReactNode
}) {
return (
<div className={hstack({
justifyContent: 'space-between',
gap: '4',
paddingY: '2',
borderBottom: '1px solid',
borderColor: 'gray.100'
})}>
<div className={hstack({ gap: '2', color: 'gray.700' })}>
{icon}
<span className={css({ fontWeight: 'medium' })}>{label}</span>
</div>
<div className={css({ textAlign: 'right', flex: '1' })}>
{value}
</div>
</div>
)
}

View File

@ -0,0 +1,115 @@
'use client'
import React, { useState, useEffect } from 'react'
import * as Dialog from '@radix-ui/react-dialog'
import { Info, X } from 'lucide-react'
import { css } from '../../styled-system/css'
interface DeploymentInfoModalProps {
children: React.ReactNode
}
export function DeploymentInfoModal({ children }: DeploymentInfoModalProps) {
const [open, setOpen] = useState(false)
useEffect(() => {
// Keyboard shortcut: Cmd/Ctrl + Shift + I
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'I') {
e.preventDefault()
setOpen(prev => !prev)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Portal>
<Dialog.Overlay className={css({
position: 'fixed',
inset: '0',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 9998,
animation: 'fadeIn 150ms cubic-bezier(0.16, 1, 0.3, 1)',
})} />
<Dialog.Content className={css({
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
borderRadius: 'lg',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
padding: '6',
width: '90vw',
maxWidth: '500px',
maxHeight: '85vh',
overflow: 'auto',
zIndex: 9999,
animation: 'contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)',
})}>
<Dialog.Title className={css({
fontSize: 'xl',
fontWeight: 'semibold',
marginBottom: '4',
display: 'flex',
alignItems: 'center',
gap: '2'
})}>
<Info size={24} />
Deployment Information
</Dialog.Title>
{children}
<div className={css({
marginTop: '6',
paddingTop: '4',
borderTop: '1px solid',
borderColor: 'gray.200',
fontSize: 'sm',
color: 'gray.600',
textAlign: 'center'
})}>
Press <kbd className={css({
fontFamily: 'mono',
backgroundColor: 'gray.100',
padding: '0.5 1.5',
borderRadius: 'sm',
border: '1px solid',
borderColor: 'gray.300'
})}>I</kbd> or <kbd className={css({
fontFamily: 'mono',
backgroundColor: 'gray.100',
padding: '0.5 1.5',
borderRadius: 'sm',
border: '1px solid',
borderColor: 'gray.300'
})}>CtrlI</kbd> to toggle
</div>
<Dialog.Close className={css({
position: 'absolute',
top: '4',
right: '4',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'md',
padding: '2',
color: 'gray.700',
cursor: 'pointer',
_hover: {
backgroundColor: 'gray.100'
}
})}>
<X size={20} />
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}