boigas
This commit is contained in:
parent
dd25203f66
commit
e2ff53e6d8
|
|
@ -5,6 +5,7 @@ import { theme } from './themes'
|
|||
|
||||
// UI state (survives re-renders)
|
||||
let selectedApp: string | null = localStorage.getItem('selectedApp')
|
||||
let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
|
||||
|
||||
// Server state (from SSE)
|
||||
let apps: App[] = []
|
||||
|
|
@ -30,12 +31,36 @@ const Logo = define('Logo', {
|
|||
height: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 16px',
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
borderBottom: `1px solid ${theme('colors-border')}`,
|
||||
})
|
||||
|
||||
const HamburgerButton = define('HamburgerButton', {
|
||||
base: 'button',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 4,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
selectors: {
|
||||
'&:hover span': { background: theme('colors-text') },
|
||||
},
|
||||
})
|
||||
|
||||
const HamburgerLine = define('HamburgerLine', {
|
||||
width: 18,
|
||||
height: 2,
|
||||
background: theme('colors-textMuted'),
|
||||
borderRadius: 1,
|
||||
transition: 'background 0.15s',
|
||||
})
|
||||
|
||||
const SectionLabel = define('SectionLabel', {
|
||||
padding: '16px 16px 8px',
|
||||
fontSize: 12,
|
||||
|
|
@ -141,7 +166,7 @@ const HeaderActions = define('HeaderActions', {
|
|||
|
||||
const MainContent = define('MainContent', {
|
||||
flex: 1,
|
||||
padding: 24,
|
||||
padding: '10px 24px',
|
||||
overflow: 'auto',
|
||||
})
|
||||
|
||||
|
|
@ -251,6 +276,40 @@ const LogTime = define('LogTime', {
|
|||
display: 'inline',
|
||||
})
|
||||
|
||||
let selectedTab: 'overview' | 'todo' = 'overview'
|
||||
|
||||
const TabContent = define('TabContent', {
|
||||
display: 'none',
|
||||
|
||||
variants: {
|
||||
active: {
|
||||
display: 'block'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const NavButton = define('NavButton', {
|
||||
render({ props }) {
|
||||
return (
|
||||
<Button onClick={props.onClick} style={{ marginRight: 10, marginBottom: 20 }}>
|
||||
{props.children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function setSelectedTab(tab: 'overview' | 'todo') {
|
||||
selectedTab = tab
|
||||
render()
|
||||
}
|
||||
|
||||
const Nav = () => {
|
||||
return <>
|
||||
<NavButton onClick={() => setSelectedTab('overview')}>Overview</NavButton>
|
||||
<NavButton onClick={() => setSelectedTab('todo')}>TODO</NavButton>
|
||||
</>
|
||||
}
|
||||
|
||||
const stateLabels: Record<AppState, string> = {
|
||||
invalid: 'Invalid',
|
||||
stopped: 'Stopped',
|
||||
|
|
@ -270,6 +329,12 @@ const selectApp = (name: string) => {
|
|||
render()
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
sidebarCollapsed = !sidebarCollapsed
|
||||
localStorage.setItem('sidebarCollapsed', String(sidebarCollapsed))
|
||||
render()
|
||||
}
|
||||
|
||||
const AppDetail = ({ app }: { app: App }) => (
|
||||
<>
|
||||
<MainHeader>
|
||||
|
|
@ -284,79 +349,87 @@ const AppDetail = ({ app }: { app: App }) => (
|
|||
</HeaderActions>
|
||||
</MainHeader>
|
||||
<MainContent>
|
||||
<Section>
|
||||
<SectionTitle>Status</SectionTitle>
|
||||
<InfoRow>
|
||||
<InfoLabel>State</InfoLabel>
|
||||
<InfoValue>
|
||||
<StatusDot state={app.state} />
|
||||
{stateLabels[app.state]}
|
||||
{app.port ? ` on :${app.port}` : ''}
|
||||
</InfoValue>
|
||||
</InfoRow>
|
||||
{app.state === 'running' && app.port && (
|
||||
<Nav />
|
||||
|
||||
<TabContent active={selectedTab === 'overview'}>
|
||||
<Section>
|
||||
<SectionTitle>Status</SectionTitle>
|
||||
<InfoRow>
|
||||
<InfoLabel>URL</InfoLabel>
|
||||
<InfoLabel>State</InfoLabel>
|
||||
<InfoValue>
|
||||
<Link href={`http://localhost:${app.port}`} target="_blank">
|
||||
http://localhost:{app.port}
|
||||
</Link>
|
||||
<StatusDot state={app.state} />
|
||||
{stateLabels[app.state]}
|
||||
{app.port ? ` on :${app.port}` : ''}
|
||||
</InfoValue>
|
||||
</InfoRow>
|
||||
)}
|
||||
{app.started && (
|
||||
<InfoRow>
|
||||
<InfoLabel>Started</InfoLabel>
|
||||
<InfoValue>{new Date(app.started).toLocaleString()}</InfoValue>
|
||||
</InfoRow>
|
||||
)}
|
||||
{app.error && (
|
||||
<InfoRow>
|
||||
<InfoLabel>Error</InfoLabel>
|
||||
<InfoValue style={{ color: theme('colors-error') }}>
|
||||
{app.error}
|
||||
</InfoValue>
|
||||
</InfoRow>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionTitle>Logs</SectionTitle>
|
||||
<LogsContainer>
|
||||
{app.logs?.length ? (
|
||||
app.logs.map((line, i) => (
|
||||
<LogLine key={i}>
|
||||
<LogTime>{new Date(line.time).toLocaleTimeString()}</LogTime>
|
||||
<span>{line.text}</span>
|
||||
</LogLine>
|
||||
))
|
||||
) : (
|
||||
<LogLine>
|
||||
<LogTime>--:--:--</LogTime>
|
||||
<span style={{ color: theme('colors-textFaint') }}>No logs yet</span>
|
||||
</LogLine>
|
||||
{app.state === 'running' && app.port && (
|
||||
<InfoRow>
|
||||
<InfoLabel>URL</InfoLabel>
|
||||
<InfoValue>
|
||||
<Link href={`http://localhost:${app.port}`} target="_blank">
|
||||
http://localhost:{app.port}
|
||||
</Link>
|
||||
</InfoValue>
|
||||
</InfoRow>
|
||||
)}
|
||||
</LogsContainer>
|
||||
</Section>
|
||||
{app.started && (
|
||||
<InfoRow>
|
||||
<InfoLabel>Started</InfoLabel>
|
||||
<InfoValue>{new Date(app.started).toLocaleString()}</InfoValue>
|
||||
</InfoRow>
|
||||
)}
|
||||
{app.error && (
|
||||
<InfoRow>
|
||||
<InfoLabel>Error</InfoLabel>
|
||||
<InfoValue style={{ color: theme('colors-error') }}>
|
||||
{app.error}
|
||||
</InfoValue>
|
||||
</InfoRow>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<ActionBar>
|
||||
{app.state === 'stopped' && (
|
||||
<Button variant="primary" onClick={() => startApp(app.name)}>
|
||||
Start
|
||||
</Button>
|
||||
)}
|
||||
{app.state === 'running' && (
|
||||
<>
|
||||
<Button onClick={() => restartApp(app.name)}>Restart</Button>
|
||||
<Button variant="danger" onClick={() => stopApp(app.name)}>
|
||||
Stop
|
||||
<Section>
|
||||
<SectionTitle>Logs</SectionTitle>
|
||||
<LogsContainer>
|
||||
{app.logs?.length ? (
|
||||
app.logs.map((line, i) => (
|
||||
<LogLine key={i}>
|
||||
<LogTime>{new Date(line.time).toLocaleTimeString()}</LogTime>
|
||||
<span>{line.text}</span>
|
||||
</LogLine>
|
||||
))
|
||||
) : (
|
||||
<LogLine>
|
||||
<LogTime>--:--:--</LogTime>
|
||||
<span style={{ color: theme('colors-textFaint') }}>No logs yet</span>
|
||||
</LogLine>
|
||||
)}
|
||||
</LogsContainer>
|
||||
</Section>
|
||||
|
||||
<ActionBar>
|
||||
{app.state === 'stopped' && (
|
||||
<Button variant="primary" onClick={() => startApp(app.name)}>
|
||||
Start
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(app.state === 'starting' || app.state === 'stopping') && (
|
||||
<Button disabled>{stateLabels[app.state]}...</Button>
|
||||
)}
|
||||
</ActionBar>
|
||||
)}
|
||||
{app.state === 'running' && (
|
||||
<>
|
||||
<Button onClick={() => restartApp(app.name)}>Restart</Button>
|
||||
<Button variant="danger" onClick={() => stopApp(app.name)}>
|
||||
Stop
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(app.state === 'starting' || app.state === 'stopping') && (
|
||||
<Button disabled>{stateLabels[app.state]}...</Button>
|
||||
)}
|
||||
</ActionBar>
|
||||
</TabContent>
|
||||
|
||||
<TabContent active={selectedTab === 'todo'}>
|
||||
<h1>hardy har har</h1>
|
||||
</TabContent>
|
||||
</MainContent>
|
||||
</>
|
||||
)
|
||||
|
|
@ -367,28 +440,39 @@ const Dashboard = () => {
|
|||
return (
|
||||
<Layout>
|
||||
<Styles />
|
||||
<Sidebar>
|
||||
<Logo>🐾 Toes</Logo>
|
||||
<SectionLabel>Apps</SectionLabel>
|
||||
<Sidebar style={sidebarCollapsed ? { width: 'auto' } : undefined}>
|
||||
<Logo>
|
||||
{!sidebarCollapsed && <span>🐾 Toes</span>}
|
||||
<HamburgerButton onClick={toggleSidebar} title={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}>
|
||||
<HamburgerLine />
|
||||
<HamburgerLine />
|
||||
<HamburgerLine />
|
||||
</HamburgerButton>
|
||||
</Logo>
|
||||
{!sidebarCollapsed && <SectionLabel>Apps</SectionLabel>}
|
||||
<AppList>
|
||||
{apps.map(app => (
|
||||
<AppItem
|
||||
key={app.name}
|
||||
onClick={() => selectApp(app.name)}
|
||||
selected={app.name === selectedApp ? true : undefined}
|
||||
style={sidebarCollapsed ? { justifyContent: 'center', padding: '10px 12px' } : undefined}
|
||||
title={sidebarCollapsed ? app.name : undefined}
|
||||
>
|
||||
{app.state === 'running' && app.icon ? (
|
||||
<span style={{ fontSize: 14 }}>{app.icon}</span>
|
||||
<span style={{ fontSize: sidebarCollapsed ? 18 : 14 }}>{app.icon}</span>
|
||||
) : (
|
||||
<StatusDot state={app.state} />
|
||||
)}
|
||||
{app.name}
|
||||
{!sidebarCollapsed && app.name}
|
||||
</AppItem>
|
||||
))}
|
||||
</AppList>
|
||||
<SidebarFooter>
|
||||
<NewAppButton>+ New App</NewAppButton>
|
||||
</SidebarFooter>
|
||||
{!sidebarCollapsed && (
|
||||
<SidebarFooter>
|
||||
<NewAppButton>+ New App</NewAppButton>
|
||||
</SidebarFooter>
|
||||
)}
|
||||
</Sidebar>
|
||||
<Main>
|
||||
{selected ? (
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user