dashboard mobile fixes
This commit is contained in:
parent
86dacb0a74
commit
caac6877d7
|
|
@ -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 · {formatPercent(totalCpu)} CPU · {formatRss(totalRss)} RSS · {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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user