toes/src/client/components/AppDetail.tsx
2026-01-30 08:23:29 -08:00

152 lines
4.5 KiB
TypeScript

import { define } from '@because/forge'
import type { App } from '../../shared/types'
import { restartApp, startApp, stopApp } from '../api'
import { openDeleteAppModal, openRenameAppModal } from '../modals'
import { selectedTab } from '../state'
import {
ActionBar,
Button,
ClickableAppName,
HeaderActions,
InfoLabel,
InfoRow,
InfoValue,
Link,
LogLine,
LogsContainer,
LogTime,
Main,
MainContent,
MainHeader,
MainTitle,
Section,
SectionTitle,
stateLabels,
StatusDot,
TabContent,
} from '../styles'
import { openEmojiPicker } from './emoji-picker'
import { theme } from '../themes'
import { Nav } from './Nav'
const OpenEmojiPicker = define('OpenEmojiPicker', {
cursor: 'pointer',
render({ props: { app, children, render: renderFn }, parts: { Root } }) {
return <Root onClick={() => openEmojiPicker((emoji) => {
if (!app) return
fetch(`/api/apps/${app.name}/icon?icon=${emoji}`, { method: 'POST' })
app.icon = emoji
renderFn()
})}>{children}</Root>
}
})
export function AppDetail({ app, render }: { app: App, render: () => void }) {
return (
<Main>
<MainHeader>
<MainTitle>
<OpenEmojiPicker app={app} render={render}>{app.icon}</OpenEmojiPicker>
&nbsp;
<ClickableAppName onClick={() => openRenameAppModal(app)}>{app.name}</ClickableAppName>
</MainTitle>
<HeaderActions>
<Button variant="danger" onClick={() => openDeleteAppModal(app)}>Delete</Button>
</HeaderActions>
</MainHeader>
<MainContent>
<Nav render={render} />
<TabContent active={selectedTab === 'overview'}>
<Section>
<SectionTitle>Status</SectionTitle>
<InfoRow>
<InfoLabel>State</InfoLabel>
<InfoValue>
<StatusDot state={app.state} />
{stateLabels[app.state]}
</InfoValue>
</InfoRow>
{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>
)}
{app.state === 'running' && app.port && (
<InfoRow>
<InfoLabel>Port</InfoLabel>
<InfoValue>
{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>
)}
</LogsContainer>
</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
</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>
</Main>
)
}