dashboard mobile fixes

This commit is contained in:
Chris Wanstrath 2026-02-16 09:22:26 -08:00
parent 86dacb0a74
commit caac6877d7
8 changed files with 171 additions and 17 deletions

View File

@ -339,6 +339,41 @@ const EmptyState = define('EmptyState', {
color: theme('colors-textMuted'), color: theme('colors-textMuted'),
}) })
const Tab = define('Tab', {
base: 'a',
padding: '8px 16px',
fontSize: '13px',
fontFamily: theme('fonts-sans'),
color: theme('colors-textMuted'),
textDecoration: 'none',
borderBottom: '2px solid transparent',
cursor: 'pointer',
states: {
':hover': {
color: theme('colors-text'),
},
},
})
const TabActive = define('TabActive', {
base: 'a',
padding: '8px 16px',
fontSize: '13px',
fontFamily: theme('fonts-sans'),
color: theme('colors-text'),
textDecoration: 'none',
borderBottom: `2px solid ${theme('colors-primary')}`,
fontWeight: 'bold',
cursor: 'default',
})
const TabBar = define('TabBar', {
display: 'flex',
gap: '4px',
borderBottom: `1px solid ${theme('colors-border')}`,
marginBottom: '15px',
})
const ChartsContainer = define('ChartsContainer', { const ChartsContainer = define('ChartsContainer', {
marginTop: '24px', marginTop: '24px',
display: 'grid', display: 'grid',
@ -487,6 +522,82 @@ app.get('/', async c => {
// Single app view // Single app view
if (appName) { if (appName) {
const tab = c.req.query('tab') === 'global' ? 'global' : 'app'
const appUrl = `/?app=${appName}`
const globalUrl = `/?app=${appName}&tab=global`
if (tab === 'global') {
const metrics = await getAppMetrics()
metrics.sort((a, b) => {
if (a.state === 'running' && b.state !== 'running') return -1
if (a.state !== 'running' && b.state === 'running') return 1
return a.name.localeCompare(b.name)
})
if (metrics.length === 0) {
return c.html(
<Layout title="Metrics">
<TabBar>
<Tab href={appUrl}>App</Tab>
<TabActive href={globalUrl}>Global</TabActive>
</TabBar>
<EmptyState>No apps found</EmptyState>
</Layout>
)
}
const running = metrics.filter(s => s.state === 'running')
const totalCpu = running.reduce((sum, s) => sum + (s.cpu ?? 0), 0)
const totalRss = running.reduce((sum, s) => sum + (s.rss ?? 0), 0)
const totalData = metrics.reduce((sum, s) => sum + (s.dataSize ?? 0), 0)
return c.html(
<Layout title="Metrics - Global">
<TabBar>
<Tab href={appUrl}>App</Tab>
<TabActive href={globalUrl}>Global</TabActive>
</TabBar>
<Table>
<thead>
<tr>
<Th>Name</Th>
<Th>State</Th>
<ThRight>PID</ThRight>
<ThRight>CPU</ThRight>
<ThRight>MEM</ThRight>
<ThRight>RSS</ThRight>
<ThRight>Data</ThRight>
</tr>
</thead>
<tbody>
{metrics.map(s => (
<Tr>
<Td>
{s.name}
{s.tool && <ToolBadge>[tool]</ToolBadge>}
</Td>
<Td>
<StatusBadge style={`color: ${getStatusColor(s.state)}`}>
{s.state}
</StatusBadge>
</Td>
<TdRight>{s.pid ?? '-'}</TdRight>
<TdRight>{formatPercent(s.cpu)}</TdRight>
<TdRight>{formatPercent(s.memory)}</TdRight>
<TdRight>{formatRss(s.rss)}</TdRight>
<TdRight>{formatBytes(s.dataSize)}</TdRight>
</Tr>
))}
</tbody>
</Table>
<Summary>
{running.length} running &middot; {formatPercent(totalCpu)} CPU &middot; {formatRss(totalRss)} RSS &middot; {formatBytes(totalData)} data
</Summary>
</Layout>
)
}
const metrics = await getAppMetricsByName(appName) const metrics = await getAppMetricsByName(appName)
if (!metrics) { if (!metrics) {
@ -499,6 +610,10 @@ app.get('/', async c => {
return c.html( return c.html(
<Layout title="Metrics"> <Layout title="Metrics">
<TabBar>
<TabActive href={appUrl}>App</TabActive>
<Tab href={globalUrl}>Global</Tab>
</TabBar>
<Table> <Table>
<thead> <thead>
<tr> <tr>

View File

@ -14,6 +14,8 @@ import { update } from '../update'
import { UnifiedLogs, initUnifiedLogs } from './UnifiedLogs' import { UnifiedLogs, initUnifiedLogs } from './UnifiedLogs'
import { Vitals, initVitals } from './Vitals' import { Vitals, initVitals } from './Vitals'
let activeTooltip: string | null = null
export function DashboardLanding({ render }: { render: () => void }) { export function DashboardLanding({ render }: { render: () => void }) {
useEffect(() => { useEffect(() => {
initUnifiedLogs() initUnifiedLogs()
@ -38,11 +40,22 @@ export function DashboardLanding({ render }: { render: () => void }) {
<StatusDotsRow> <StatusDotsRow>
{[...apps.filter(a => !a.tool), ...apps.filter(a => a.tool)].map(app => ( {[...apps.filter(a => !a.tool), ...apps.filter(a => a.tool)].map(app => (
<StatusDotLink key={app.name} data-tooltip={app.name} onClick={(e: Event) => { <StatusDotLink
e.preventDefault() key={app.name}
setSelectedApp(app.name) data-tooltip={app.name}
update() tooltipVisible={activeTooltip === app.name || undefined}
}}> onClick={(e: Event) => {
e.preventDefault()
if (isNarrow && activeTooltip !== app.name) {
activeTooltip = app.name
render()
return
}
activeTooltip = null
setSelectedApp(app.name)
update()
}}
>
<StatusDot state={app.state} data-app={app.name} /> <StatusDot state={app.state} data-app={app.name} />
</StatusDotLink> </StatusDotLink>
))} ))}

View File

@ -206,7 +206,7 @@ export function LogsSection({ app }: { app: App }) {
} }
return ( return (
<Section> <Section style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, marginBottom: 0 }}>
<LogsHeader> <LogsHeader>
<SectionTitle style={{ marginBottom: 0 }}>Logs</SectionTitle> <SectionTitle style={{ marginBottom: 0 }}>Logs</SectionTitle>
<LogsControls> <LogsControls>

View File

@ -127,17 +127,18 @@ function Gauge({ value }: { value: number }) {
} }
function VitalsContent() { function VitalsContent() {
const narrow = isNarrow || undefined
return ( return (
<> <>
<VitalCard> <VitalCard narrow={narrow}>
<VitalLabel>CPU</VitalLabel> <VitalLabel>CPU</VitalLabel>
<Gauge value={_metrics.cpu} /> <Gauge value={_metrics.cpu} />
</VitalCard> </VitalCard>
<VitalCard> <VitalCard narrow={narrow}>
<VitalLabel>RAM</VitalLabel> <VitalLabel>RAM</VitalLabel>
<Gauge value={_metrics.ram.percent} /> <Gauge value={_metrics.ram.percent} />
</VitalCard> </VitalCard>
<VitalCard> <VitalCard narrow={narrow}>
<VitalLabel>Disk</VitalLabel> <VitalLabel>Disk</VitalLabel>
<Gauge value={_metrics.disk.percent} /> <Gauge value={_metrics.disk.percent} />
</VitalCard> </VitalCard>

View File

@ -10,7 +10,7 @@ export const VitalsSection = define('VitalsSection', {
maxWidth: 800, maxWidth: 800,
variants: { variants: {
narrow: { narrow: {
gridTemplateColumns: '1fr', gap: 12,
}, },
}, },
}) })
@ -24,6 +24,12 @@ export const VitalCard = define('VitalCard', {
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
gap: 16, gap: 16,
variants: {
narrow: {
padding: 12,
gap: 8,
},
},
}) })
export const VitalLabel = define('VitalLabel', { export const VitalLabel = define('VitalLabel', {
@ -60,6 +66,10 @@ export const GaugeValue = define('GaugeValue', {
export const LogsSection = define('LogsSection', { export const LogsSection = define('LogsSection', {
width: '100%', width: '100%',
maxWidth: 800, maxWidth: 800,
flex: 1,
minHeight: 0,
display: 'flex',
flexDirection: 'column',
background: theme('colors-bgElement'), background: theme('colors-bgElement'),
border: `1px solid ${theme('colors-border')}`, border: `1px solid ${theme('colors-border')}`,
borderRadius: theme('radius-md'), borderRadius: theme('radius-md'),
@ -72,6 +82,7 @@ export const LogsHeader = define('LogsHeader', {
alignItems: 'center', alignItems: 'center',
padding: '12px 16px', padding: '12px 16px',
borderBottom: `1px solid ${theme('colors-border')}`, borderBottom: `1px solid ${theme('colors-border')}`,
flexShrink: 0,
}) })
export const LogsTitle = define('LogsTitle', { export const LogsTitle = define('LogsTitle', {
@ -133,7 +144,8 @@ export const LogsTab = define('LogsTab', {
}) })
export const LogsBody = define('LogsBody', { export const LogsBody = define('LogsBody', {
height: 200, flex: 1,
minHeight: 0,
overflow: 'auto', overflow: 'auto',
fontFamily: theme('fonts-mono'), fontFamily: theme('fonts-mono'),
fontSize: 12, fontSize: 12,

View File

@ -192,8 +192,10 @@ export const HeaderActions = define('HeaderActions', {
export const MainContent = define('MainContent', { export const MainContent = define('MainContent', {
flex: 1, flex: 1,
display: 'flex',
flexDirection: 'column',
padding: '10px 24px', padding: '10px 24px',
overflow: 'auto', overflow: 'hidden',
}) })
export const DashboardContainer = define('DashboardContainer', { export const DashboardContainer = define('DashboardContainer', {
@ -201,14 +203,12 @@ export const DashboardContainer = define('DashboardContainer', {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
padding: 40, padding: 40,
paddingTop: 0,
gap: 40, gap: 40,
overflow: 'hidden',
variants: { variants: {
narrow: { narrow: {
padding: 20, padding: 20,
paddingTop: 0,
gap: 24, gap: 24,
}, },
}, },

View File

@ -8,7 +8,8 @@ export const LogsContainer = define('LogsContainer', {
fontFamily: theme('fonts-mono'), fontFamily: theme('fonts-mono'),
fontSize: 12, fontSize: 12,
color: theme('colors-textMuted'), color: theme('colors-textMuted'),
maxHeight: 200, flex: 1,
minHeight: 0,
overflow: 'auto', overflow: 'auto',
variants: { variants: {
narrow: { narrow: {

View File

@ -31,6 +31,15 @@ export const StatusDotLink = define('StatusDotLink', {
opacity: 1, opacity: 1,
}, },
}, },
variants: {
tooltipVisible: {
selectors: {
'&::after': {
opacity: 1,
},
},
},
},
}) })
export const StatusDotsRow = define('StatusDotsRow', { export const StatusDotsRow = define('StatusDotsRow', {
@ -143,10 +152,13 @@ export const Tab = define('Tab', {
export const TabContent = define('TabContent', { export const TabContent = define('TabContent', {
display: 'none', display: 'none',
minHeight: 0,
variants: { variants: {
active: { active: {
display: 'block' display: 'flex',
flexDirection: 'column',
flex: 1,
} }
} }
}) })