toes/src/client/components/AppDetail.tsx

185 lines
5.9 KiB
TypeScript

import { define } from '@because/forge'
import type { App } from '../../shared/types'
import { disableTunnel, enableTunnel, restartApp, startApp, stopApp } from '../api'
import { openAppSelectorModal, openDeleteAppModal, openRenameAppModal } from '../modals'
import { apps, getSelectedTab, isNarrow } from '../state'
import {
ActionBar,
AppSelectorChevron,
Button,
ClickableAppName,
HeaderActions,
InfoLabel,
InfoRow,
InfoValue,
Link,
Main,
MainContent,
MainHeader,
MainTitle,
Section,
SectionTitle,
stateLabels,
StatusDot,
TabContent,
} from '../styles'
import { openEmojiPicker } from './emoji-picker'
import { LogsSection } from './LogsSection'
import { Nav } from './Nav'
import { theme } from '../themes'
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 }) {
// Find all tools
const tools = apps.filter(a => a.tool)
const selectedTab = getSelectedTab(app.name)
return (
<Main>
<MainHeader>
<MainTitle>
<OpenEmojiPicker app={app} render={render}>{app.icon}</OpenEmojiPicker>
&nbsp;
<ClickableAppName onClick={() => openRenameAppModal(app)}>{app.name}</ClickableAppName>
{isNarrow && (
<AppSelectorChevron onClick={() => openAppSelectorModal(render)}>
</AppSelectorChevron>
)}
</MainTitle>
<HeaderActions>
{!app.tool && (
app.tunnelUrl
? <Button onClick={() => { disableTunnel(app.name) }}>Unshare</Button>
: app.tunnelEnabled
? <Button disabled>Sharing...</Button>
: <Button disabled={app.state !== 'running'} onClick={() => { enableTunnel(app.name) }}>Share</Button>
)}
<Button variant="danger" onClick={() => openDeleteAppModal(app)}>Delete</Button>
</HeaderActions>
</MainHeader>
<MainContent>
<Nav app={app} 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={`${location.protocol}//${location.hostname}:${app.port}`} target="_blank">
{location.protocol}//{location.hostname}:{app.port}
</Link>
</InfoValue>
</InfoRow>
)}
{app.tunnelUrl && (
<InfoRow>
<InfoLabel>Tunnel</InfoLabel>
<InfoValue>
<Link href={app.tunnelUrl} target="_blank">{app.tunnelUrl}</Link>
</InfoValue>
</InfoRow>
)}
{app.state === 'running' && app.port && (
<InfoRow>
<InfoLabel>Port</InfoLabel>
<InfoValue>
{app.port}
</InfoValue>
</InfoRow>
)}
{app.state === 'running' && app.pid && (
<InfoRow>
<InfoLabel>PID</InfoLabel>
<InfoValue>
{app.pid}
</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>
<LogsSection app={app} />
<ActionBar>
{(app.state === 'stopped' || app.state === 'error') && (
<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>
{tools.map(tool => {
const toolName = typeof tool.tool === 'string' ? tool.tool : tool.name
const isSelected = selectedTab === tool.name
return (
<TabContent key={tool.name} active={isSelected}>
<Section>
{tool.state !== 'running' && (
<p style={{ color: theme('colors-textFaint') }}>
Tool is {stateLabels[tool.state].toLowerCase()}
</p>
)}
{/* Target for iframe overlay positioning */}
{tool.state === 'running' && (
<div
data-tool-target={isSelected ? tool.name : undefined}
style={{ width: '100%', height: '600px' }}
/>
)}
</Section>
</TabContent>
)
})}
</MainContent>
</Main>
)
}