fix(devops): responsive UI + overflow guards in DevopsPanel
Some checks are pending
CI — Common Platform / Build, Test & Typecheck (push) Waiting to run
CI — Common Platform / Publish @bytelyst/* to Gitea npm registry (push) Blocked by required conditions

@bytelyst/devops 0.1.3:
- Wrap tables in scrollable container (overflow-x: auto) so long values
  never push the panel wider than its parent.
- table-layout: fixed + min-width on key column prevents column blow-out.
- Code/value cells use overflow-wrap: anywhere + word-break: break-word
  so long commit SHAs and Docker image strings wrap.
- pre block uses pre-wrap + break-word for raw JSON.
- Header actions wrap on narrow viewports.
- Tabs row scrolls horizontally rather than wrapping awkwardly.
- root container: maxWidth 100% + minWidth 0 to play nicely with flex parents.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
root 2026-05-10 07:14:29 +00:00
parent 51fc8d09b0
commit d2420f5d3c
2 changed files with 64 additions and 41 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@bytelyst/devops", "name": "@bytelyst/devops",
"version": "0.1.2", "version": "0.1.3",
"type": "module", "type": "module",
"description": "Runtime devops metadata (build info, uptime, health) — server collector + React UI", "description": "Runtime devops metadata (build info, uptime, health) — server collector + React UI",
"exports": { "exports": {

View File

@ -231,18 +231,20 @@ interface KvRow {
function KvTable({ rows }: { rows: KvRow[] }): React.ReactElement { function KvTable({ rows }: { rows: KvRow[] }): React.ReactElement {
return ( return (
<table style={styles.table}> <div style={styles.tableWrap}>
<tbody> <table style={styles.table}>
{rows.map((r, i) => ( <tbody>
<tr key={i} style={styles.tr}> {rows.map((r, i) => (
<th style={styles.th} scope="row"> <tr key={i} style={styles.tr}>
{r.key} <th style={styles.th} scope="row">
</th> {r.key}
<td style={styles.td}>{r.value}</td> </th>
</tr> <td style={styles.td}>{r.value}</td>
))} </tr>
</tbody> ))}
</table> </tbody>
</table>
</div>
); );
} }
@ -252,30 +254,32 @@ function DependenciesTable({
deps: NonNullable<DevopsInfo['dependencies']>; deps: NonNullable<DevopsInfo['dependencies']>;
}): React.ReactElement { }): React.ReactElement {
return ( return (
<table style={styles.table}> <div style={styles.tableWrap}>
<thead> <table style={styles.table}>
<tr style={styles.tr}> <thead>
<th style={{ ...styles.th, textAlign: 'left' }}>Name</th> <tr style={styles.tr}>
<th style={{ ...styles.th, textAlign: 'left' }}>Status</th> <th style={{ ...styles.th, textAlign: 'left' }}>Name</th>
<th style={{ ...styles.th, textAlign: 'left' }}>Latency</th> <th style={{ ...styles.th, textAlign: 'left' }}>Status</th>
<th style={{ ...styles.th, textAlign: 'left' }}>Detail</th> <th style={{ ...styles.th, textAlign: 'left' }}>Latency</th>
</tr> <th style={{ ...styles.th, textAlign: 'left' }}>Detail</th>
</thead>
<tbody>
{deps.map((d, i) => (
<tr key={i} style={styles.tr}>
<td style={styles.td}>{d.name}</td>
<td style={styles.td}>
<span style={{ ...styles.badge, ...(d.ok ? styles.badgeOk : styles.badgeErr) }}>
{d.ok ? 'OK' : 'FAIL'}
</span>
</td>
<td style={styles.td}>{d.latencyMs != null ? `${d.latencyMs} ms` : '—'}</td>
<td style={styles.td}>{d.detail ?? '—'}</td>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {deps.map((d, i) => (
<tr key={i} style={styles.tr}>
<td style={styles.td}>{d.name}</td>
<td style={styles.td}>
<span style={{ ...styles.badge, ...(d.ok ? styles.badgeOk : styles.badgeErr) }}>
{d.ok ? 'OK' : 'FAIL'}
</span>
</td>
<td style={styles.td}>{d.latencyMs != null ? `${d.latencyMs} ms` : '—'}</td>
<td style={styles.td}>{d.detail ?? '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
); );
} }
@ -289,7 +293,7 @@ function formatTimestamp(iso: string): string {
} }
const styles: Record<string, React.CSSProperties> = { const styles: Record<string, React.CSSProperties> = {
root: { display: 'flex', flexDirection: 'column', gap: 12 }, root: { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: '100%', minWidth: 0 },
header: { header: {
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
@ -300,7 +304,7 @@ const styles: Record<string, React.CSSProperties> = {
title: { fontSize: 16, fontWeight: 600, color: 'var(--foreground, inherit)' }, title: { fontSize: 16, fontWeight: 600, color: 'var(--foreground, inherit)' },
subtitle: { fontSize: 13, color: 'var(--muted-foreground, #6b7280)', marginTop: 4 }, subtitle: { fontSize: 13, color: 'var(--muted-foreground, #6b7280)', marginTop: 4 },
muted: { fontSize: 12, color: 'var(--muted-foreground, #6b7280)' }, muted: { fontSize: 12, color: 'var(--muted-foreground, #6b7280)' },
headerActions: { display: 'flex', gap: 8, alignItems: 'center' }, headerActions: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' },
sourceToggle: { sourceToggle: {
display: 'inline-flex', display: 'inline-flex',
border: '1px solid var(--border, #e5e7eb)', border: '1px solid var(--border, #e5e7eb)',
@ -332,6 +336,8 @@ const styles: Record<string, React.CSSProperties> = {
display: 'flex', display: 'flex',
gap: 2, gap: 2,
borderBottom: '1px solid var(--border, #e5e7eb)', borderBottom: '1px solid var(--border, #e5e7eb)',
overflowX: 'auto',
whiteSpace: 'nowrap',
}, },
tab: { tab: {
padding: '8px 14px', padding: '8px 14px',
@ -341,14 +347,21 @@ const styles: Record<string, React.CSSProperties> = {
borderBottom: '2px solid transparent', borderBottom: '2px solid transparent',
cursor: 'pointer', cursor: 'pointer',
color: 'var(--muted-foreground, #6b7280)', color: 'var(--muted-foreground, #6b7280)',
flexShrink: 0,
}, },
tabActive: { tabActive: {
color: 'var(--foreground, inherit)', color: 'var(--foreground, inherit)',
borderBottomColor: 'var(--accent, #2563eb)', borderBottomColor: 'var(--accent, #2563eb)',
fontWeight: 500, fontWeight: 500,
}, },
panel: { paddingTop: 8 }, panel: { paddingTop: 8, minWidth: 0 },
table: { width: '100%', borderCollapse: 'collapse', fontSize: 13 }, tableWrap: {
width: '100%',
overflowX: 'auto',
overflowY: 'visible',
WebkitOverflowScrolling: 'touch',
},
table: { width: '100%', borderCollapse: 'collapse', fontSize: 13, tableLayout: 'fixed' },
tr: { borderBottom: '1px solid var(--border, #f3f4f6)' }, tr: { borderBottom: '1px solid var(--border, #f3f4f6)' },
th: { th: {
textAlign: 'left', textAlign: 'left',
@ -357,14 +370,22 @@ const styles: Record<string, React.CSSProperties> = {
padding: '8px 12px 8px 0', padding: '8px 12px 8px 0',
verticalAlign: 'top', verticalAlign: 'top',
width: '30%', width: '30%',
minWidth: 120,
},
td: {
padding: '8px 0',
verticalAlign: 'top',
color: 'var(--foreground, inherit)',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}, },
td: { padding: '8px 0', verticalAlign: 'top', color: 'var(--foreground, inherit)' },
code: { code: {
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
fontSize: 12, fontSize: 12,
background: 'color-mix(in oklab, var(--foreground, #000) 5%, transparent)', background: 'color-mix(in oklab, var(--foreground, #000) 5%, transparent)',
padding: '2px 6px', padding: '2px 6px',
borderRadius: 4, borderRadius: 4,
wordBreak: 'break-all',
}, },
pre: { pre: {
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
@ -374,6 +395,8 @@ const styles: Record<string, React.CSSProperties> = {
borderRadius: 6, borderRadius: 6,
overflow: 'auto', overflow: 'auto',
maxHeight: 480, maxHeight: 480,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}, },
badge: { badge: {
display: 'inline-block', display: 'inline-block',