141 lines
3.0 KiB
TypeScript
141 lines
3.0 KiB
TypeScript
import { define, Styles } from 'forge'
|
|
import { allApps } from '../server/apps'
|
|
import type { AppState } from '../shared/types'
|
|
|
|
const Apps = define('Apps', {
|
|
margin: '0 auto',
|
|
width: 750,
|
|
paddingTop: 20,
|
|
})
|
|
|
|
const color = '#00c0c9'
|
|
const hoverColor = 'magenta'
|
|
|
|
const Link = define({
|
|
base: 'a',
|
|
color,
|
|
textDecoration: 'none',
|
|
borderBottom: `1px solid ${color}`,
|
|
selectors: {
|
|
'&:hover': {
|
|
color: hoverColor,
|
|
cursor: 'pointer'
|
|
}
|
|
}
|
|
})
|
|
|
|
const AppCard = define('AppCard', {
|
|
marginBottom: 24,
|
|
padding: 16,
|
|
border: '1px solid #333',
|
|
borderRadius: 8,
|
|
})
|
|
|
|
const AppHeader = define('AppHeader', {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
marginBottom: 8,
|
|
})
|
|
|
|
const AppName = define('AppName', {
|
|
fontSize: 20,
|
|
fontWeight: 'bold',
|
|
margin: 0,
|
|
})
|
|
|
|
const State = define('State', {
|
|
fontSize: 14,
|
|
padding: '2px 8px',
|
|
borderRadius: 4,
|
|
|
|
variants: {
|
|
status: {
|
|
invalid: { background: '#4a1c1c', color: '#f87171' },
|
|
stopped: { background: '#3a3a3a', color: '#9ca3af' },
|
|
starting: { background: '#3b3117', color: '#fbbf24' },
|
|
running: { background: '#14532d', color: '#4ade80' },
|
|
stopping: { background: '#3b3117', color: '#fbbf24' },
|
|
}
|
|
}
|
|
})
|
|
|
|
const Info = define('Info', {
|
|
fontSize: 14,
|
|
color: '#9ca3af',
|
|
margin: '4px 0',
|
|
})
|
|
|
|
const ActionBar = define('ActionBar', {
|
|
marginTop: 12,
|
|
display: 'flex',
|
|
gap: 8,
|
|
})
|
|
|
|
const Button = define({
|
|
base: 'button',
|
|
|
|
selectors: {
|
|
'form:has(>&)': {
|
|
display: 'inline'
|
|
}
|
|
},
|
|
|
|
render({ props, parts: { Root } }) {
|
|
if (!props.post)
|
|
return <Root>{props.children}</Root>
|
|
|
|
return (
|
|
<form method='post' action={props.post}>
|
|
<Root onClick="this.closest('form').submit()">{props.children}</Root>
|
|
</form>
|
|
)
|
|
}
|
|
})
|
|
|
|
const stateLabels: Record<AppState, string> = {
|
|
invalid: 'Invalid',
|
|
stopped: 'Stopped',
|
|
starting: 'Starting...',
|
|
running: 'Running',
|
|
stopping: 'Stopping...',
|
|
}
|
|
|
|
export default () => (
|
|
<Apps>
|
|
<Styles />
|
|
<h1>🐾 Apps</h1>
|
|
{allApps().map(app => (
|
|
<AppCard>
|
|
<AppHeader>
|
|
<AppName>
|
|
{app.state === 'running' && app.port ? (
|
|
<Link href={`http://localhost:${app.port}`}>{app.name}</Link>
|
|
) : (
|
|
app.name
|
|
)}
|
|
</AppName>
|
|
<State status={app.state}>{stateLabels[app.state]}</State>
|
|
</AppHeader>
|
|
|
|
{app.port && <Info>Port: {app.port}</Info>}
|
|
{app.started && <Info>Started: {new Date(app.started).toLocaleString()}</Info>}
|
|
|
|
<ActionBar>
|
|
{app.state === 'stopped' && (
|
|
<Button post={`/apps/${app.name}/start`}>Start</Button>
|
|
)}
|
|
{app.state === 'running' && (
|
|
<>
|
|
<Button post={`/apps/${app.name}/stop`}>Stop</Button>
|
|
<Button post={`/apps/${app.name}/restart`}>Restart</Button>
|
|
</>
|
|
)}
|
|
{app.state === 'invalid' && (
|
|
<Info>Missing or invalid package.json</Info>
|
|
)}
|
|
</ActionBar>
|
|
</AppCard>
|
|
))}
|
|
</Apps>
|
|
) |