Add tabbed dashboard with URLs/Logs/Metrics views
This commit is contained in:
parent
488c643342
commit
271bf018b8
|
|
@ -1,26 +1,25 @@
|
|||
import { useEffect } from 'hono/jsx'
|
||||
import { openAppSelectorModal } from '../modals'
|
||||
import { apps, isNarrow, setCurrentView, setSelectedApp } from '../state'
|
||||
import { dashboardTab, isNarrow, setCurrentView, setDashboardTab, setSelectedApp } from '../state'
|
||||
import {
|
||||
AppSelectorChevron,
|
||||
DashboardContainer,
|
||||
DashboardHeader,
|
||||
DashboardTitle,
|
||||
SettingsGear,
|
||||
StatusDot,
|
||||
StatusDotLink,
|
||||
StatusDotsRow,
|
||||
Tab,
|
||||
TabBar,
|
||||
TabContent,
|
||||
} from '../styles'
|
||||
import { update } from '../update'
|
||||
import { UnifiedLogs, initUnifiedLogs } from './UnifiedLogs'
|
||||
import { UnifiedLogs, initUnifiedLogs, scrollLogsToBottom } from './UnifiedLogs'
|
||||
import { Urls } from './Urls'
|
||||
import { Vitals, initVitals } from './Vitals'
|
||||
|
||||
let activeTooltip: string | null = null
|
||||
|
||||
export function DashboardLanding({ render }: { render: () => void }) {
|
||||
useEffect(() => {
|
||||
initUnifiedLogs()
|
||||
initVitals()
|
||||
if (dashboardTab === 'logs') scrollLogsToBottom()
|
||||
}, [])
|
||||
|
||||
const narrow = isNarrow || undefined
|
||||
|
|
@ -31,6 +30,12 @@ export function DashboardLanding({ render }: { render: () => void }) {
|
|||
render()
|
||||
}
|
||||
|
||||
const switchTab = (tab: typeof dashboardTab) => {
|
||||
setDashboardTab(tab)
|
||||
render()
|
||||
if (tab === 'logs') scrollLogsToBottom()
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardContainer narrow={narrow} relative>
|
||||
<SettingsGear
|
||||
|
|
@ -51,32 +56,23 @@ export function DashboardLanding({ render }: { render: () => void }) {
|
|||
</DashboardTitle>
|
||||
</DashboardHeader>
|
||||
|
||||
<StatusDotsRow>
|
||||
{[...apps.filter(a => !a.tool), ...apps.filter(a => a.tool)].map(app => (
|
||||
<StatusDotLink
|
||||
key={app.name}
|
||||
data-tooltip={app.name}
|
||||
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} />
|
||||
</StatusDotLink>
|
||||
))}
|
||||
</StatusDotsRow>
|
||||
<TabBar centered>
|
||||
<Tab active={dashboardTab === 'urls' || undefined} onClick={() => switchTab('urls')}>URLs</Tab>
|
||||
<Tab active={dashboardTab === 'logs' || undefined} onClick={() => switchTab('logs')}>Logs</Tab>
|
||||
<Tab active={dashboardTab === 'metrics' || undefined} onClick={() => switchTab('metrics')}>Metrics</Tab>
|
||||
</TabBar>
|
||||
|
||||
<Vitals />
|
||||
<TabContent active={dashboardTab === 'urls' || undefined}>
|
||||
<Urls />
|
||||
</TabContent>
|
||||
|
||||
<UnifiedLogs />
|
||||
<TabContent active={dashboardTab === 'logs' || undefined}>
|
||||
<UnifiedLogs />
|
||||
</TabContent>
|
||||
|
||||
<TabContent active={dashboardTab === 'metrics' || undefined}>
|
||||
<Vitals />
|
||||
</TabContent>
|
||||
</DashboardContainer>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,13 @@ function renderLogs() {
|
|||
})
|
||||
}
|
||||
|
||||
export function scrollLogsToBottom() {
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById('unified-logs-body')
|
||||
if (el) el.scrollTop = el.scrollHeight
|
||||
})
|
||||
}
|
||||
|
||||
export function initUnifiedLogs() {
|
||||
if (_source) return
|
||||
_source = new EventSource('/api/system/logs/stream')
|
||||
|
|
|
|||
42
src/client/components/Urls.tsx
Normal file
42
src/client/components/Urls.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { buildAppUrl } from '../../shared/urls'
|
||||
import { apps } from '../state'
|
||||
import {
|
||||
EmptyState,
|
||||
StatusDot,
|
||||
UrlLeft,
|
||||
UrlLink,
|
||||
UrlList,
|
||||
UrlPort,
|
||||
UrlRow,
|
||||
} from '../styles'
|
||||
|
||||
export function Urls() {
|
||||
const nonTools = apps.filter(a => !a.tool)
|
||||
|
||||
if (nonTools.length === 0) {
|
||||
return <EmptyState>No apps installed</EmptyState>
|
||||
}
|
||||
|
||||
return (
|
||||
<UrlList>
|
||||
{nonTools.map(app => {
|
||||
const url = buildAppUrl(app.name, location.origin)
|
||||
const running = app.state === 'running'
|
||||
|
||||
return (
|
||||
<UrlRow key={app.name}>
|
||||
<UrlLeft>
|
||||
<StatusDot state={app.state} />
|
||||
{running ? (
|
||||
<UrlLink href={url} target="_blank">{url}</UrlLink>
|
||||
) : (
|
||||
<span style={{ color: 'var(--colors-textFaint)' }}>{app.name}</span>
|
||||
)}
|
||||
</UrlLeft>
|
||||
{app.port ? <UrlPort>:{app.port}</UrlPort> : null}
|
||||
</UrlRow>
|
||||
)
|
||||
})}
|
||||
</UrlList>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import type { App } from '../shared/types'
|
||||
|
||||
export type DashboardTab = 'urls' | 'logs' | 'metrics'
|
||||
|
||||
// UI state (survives re-renders)
|
||||
export let currentView: 'dashboard' | 'settings' = 'dashboard'
|
||||
export let isNarrow: boolean = window.matchMedia('(max-width: 768px)').matches
|
||||
export let selectedApp: string | null = localStorage.getItem('selectedApp')
|
||||
export let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
|
||||
export let dashboardTab: DashboardTab = (localStorage.getItem('dashboardTab') as DashboardTab) || 'urls'
|
||||
export let sidebarSection: 'apps' | 'tools' = (localStorage.getItem('sidebarSection') as 'apps' | 'tools') || 'apps'
|
||||
|
||||
// Server state (from SSE)
|
||||
|
|
@ -14,6 +17,11 @@ export let apps: App[] = []
|
|||
export let appTabs: Record<string, string> = JSON.parse(localStorage.getItem('appTabs') || '{}')
|
||||
|
||||
// State setters
|
||||
export function setDashboardTab(tab: DashboardTab) {
|
||||
dashboardTab = tab
|
||||
localStorage.setItem('dashboardTab', tab)
|
||||
}
|
||||
|
||||
export function setCurrentView(view: 'dashboard' | 'settings') {
|
||||
currentView = view
|
||||
}
|
||||
|
|
|
|||
|
|
@ -202,3 +202,59 @@ export const LogStatus = define('LogStatus', {
|
|||
warning: { color: '#f59e0b' },
|
||||
},
|
||||
})
|
||||
|
||||
// URL List
|
||||
export const UrlLeft = define('UrlLeft', {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
minWidth: 0,
|
||||
})
|
||||
|
||||
export const UrlLink = define('UrlLink', {
|
||||
base: 'a',
|
||||
color: theme('colors-link'),
|
||||
textDecoration: 'none',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
selectors: {
|
||||
'&:hover': { textDecoration: 'underline' },
|
||||
},
|
||||
})
|
||||
|
||||
export const UrlList = define('UrlList', {
|
||||
width: '100%',
|
||||
minWidth: 400,
|
||||
maxWidth: 800,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: theme('colors-bgElement'),
|
||||
border: `1px solid ${theme('colors-border')}`,
|
||||
borderRadius: theme('radius-md'),
|
||||
overflow: 'hidden',
|
||||
})
|
||||
|
||||
export const UrlPort = define('UrlPort', {
|
||||
fontFamily: theme('fonts-mono'),
|
||||
fontSize: 12,
|
||||
color: theme('colors-textFaint'),
|
||||
flexShrink: 0,
|
||||
})
|
||||
|
||||
export const UrlRow = define('UrlRow', {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '10px 16px',
|
||||
fontFamily: theme('fonts-mono'),
|
||||
fontSize: 13,
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: theme('colors-bgHover'),
|
||||
},
|
||||
'&:not(:last-child)': {
|
||||
borderBottom: `1px solid ${theme('colors-border')}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ export {
|
|||
LogStatus,
|
||||
LogText,
|
||||
LogTimestamp,
|
||||
UrlLeft,
|
||||
UrlLink,
|
||||
UrlList,
|
||||
UrlPort,
|
||||
UrlRow,
|
||||
VitalCard,
|
||||
VitalLabel,
|
||||
VitalsSection,
|
||||
|
|
|
|||
|
|
@ -127,6 +127,12 @@ export const TabBar = define('TabBar', {
|
|||
display: 'flex',
|
||||
gap: 24,
|
||||
marginBottom: 20,
|
||||
variants: {
|
||||
centered: {
|
||||
justifyContent: 'center',
|
||||
marginBottom: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const Tab = define('Tab', {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user