partial updating
This commit is contained in:
parent
5d898ac485
commit
2c8fff85f4
|
|
@ -3,14 +3,13 @@ import { define, Styles } from 'forge'
|
||||||
import type { App, AppState } from '../shared/types'
|
import type { App, AppState } from '../shared/types'
|
||||||
import { theme } from './themes'
|
import { theme } from './themes'
|
||||||
import { Modal, initModal } from './tags/modal'
|
import { Modal, initModal } from './tags/modal'
|
||||||
|
import { initUpdate } from './update'
|
||||||
import { openEmojiPicker } from './tags/emoji-picker'
|
import { openEmojiPicker } from './tags/emoji-picker'
|
||||||
|
|
||||||
// UI state (survives re-renders)
|
// UI state (survives re-renders)
|
||||||
let selectedApp: string | null = localStorage.getItem('selectedApp')
|
let selectedApp: string | null = localStorage.getItem('selectedApp')
|
||||||
let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
|
let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
|
||||||
|
|
||||||
const DEFAULT_EMOJI = '🖥️'
|
|
||||||
|
|
||||||
// Server state (from SSE)
|
// Server state (from SSE)
|
||||||
let apps: App[] = []
|
let apps: App[] = []
|
||||||
|
|
||||||
|
|
@ -382,7 +381,7 @@ const AppDetail = ({ app }: { app: App }) => (
|
||||||
<>
|
<>
|
||||||
<MainHeader>
|
<MainHeader>
|
||||||
<MainTitle>
|
<MainTitle>
|
||||||
<OpenEmojiPicker app={app}>{app.icon ?? DEFAULT_EMOJI}</OpenEmojiPicker>
|
<OpenEmojiPicker app={app}>{app.icon}</OpenEmojiPicker>
|
||||||
|
|
||||||
{app.name}
|
{app.name}
|
||||||
</MainTitle>
|
</MainTitle>
|
||||||
|
|
@ -510,10 +509,10 @@ const Dashboard = () => {
|
||||||
title={sidebarCollapsed ? app.name : undefined}
|
title={sidebarCollapsed ? app.name : undefined}
|
||||||
>
|
>
|
||||||
{sidebarCollapsed ? (
|
{sidebarCollapsed ? (
|
||||||
<span style={{ fontSize: 18 }}>{app.icon ?? DEFAULT_EMOJI}</span>
|
<span style={{ fontSize: 18 }}>{app.icon}</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span style={{ fontSize: 14 }}>{app.icon ?? DEFAULT_EMOJI}</span>
|
<span style={{ fontSize: 14 }}>{app.icon}</span>
|
||||||
{app.name}
|
{app.name}
|
||||||
<StatusDot state={app.state} style={{ marginLeft: 'auto' }} />
|
<StatusDot state={app.state} style={{ marginLeft: 'auto' }} />
|
||||||
</>
|
</>
|
||||||
|
|
@ -543,8 +542,9 @@ const render = () => {
|
||||||
renderApp(<Dashboard />, document.getElementById('app')!)
|
renderApp(<Dashboard />, document.getElementById('app')!)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize modal with render function
|
// Initialize render functions
|
||||||
initModal(render)
|
initModal(render)
|
||||||
|
initUpdate(render)
|
||||||
|
|
||||||
// Set theme based on system preference
|
// Set theme based on system preference
|
||||||
const setTheme = () => {
|
const setTheme = () => {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
25
src/client/update.tsx
Normal file
25
src/client/update.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { render } from 'hono/jsx/dom'
|
||||||
|
import type { Child } from 'hono/jsx'
|
||||||
|
|
||||||
|
let globalRenderFn: (() => void) | null = null
|
||||||
|
|
||||||
|
export const initUpdate = (renderFn: () => void) => {
|
||||||
|
globalRenderFn = renderFn
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the UI from state.
|
||||||
|
*
|
||||||
|
* update() - redraw everything
|
||||||
|
* update('#emoji-results', <Results />) - target specific element
|
||||||
|
*/
|
||||||
|
export function update(): void
|
||||||
|
export function update(selector: string, component: Child): void
|
||||||
|
export function update(selector?: string, component?: Child) {
|
||||||
|
if (selector && component !== undefined) {
|
||||||
|
const el = document.querySelector(selector) as HTMLElement | null
|
||||||
|
if (el) render(component, el)
|
||||||
|
} else {
|
||||||
|
globalRenderFn?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import type { App as SharedApp, AppState, LogLine } from '../shared/types'
|
||||||
|
|
||||||
export type { AppState } from '../shared/types'
|
export type { AppState } from '../shared/types'
|
||||||
|
|
||||||
|
const DEFAULT_EMOJI = '🖥️'
|
||||||
const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')
|
const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')
|
||||||
const MAX_LOGS = 100
|
const MAX_LOGS = 100
|
||||||
|
|
||||||
|
|
@ -42,29 +43,20 @@ const allAppDirs = () => {
|
||||||
.sort()
|
.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns names of valid apps (those with scripts.toes in package.json) */
|
|
||||||
export const appNames = () => allAppDirs().filter(isApp)
|
|
||||||
|
|
||||||
let NEXT_PORT = 3001
|
let NEXT_PORT = 3001
|
||||||
const getPort = () => NEXT_PORT++
|
const getPort = () => NEXT_PORT++
|
||||||
|
|
||||||
/** Discover all apps and set initial states */
|
|
||||||
const discoverApps = () => {
|
const discoverApps = () => {
|
||||||
for (const dir of allAppDirs()) {
|
for (const dir of allAppDirs()) {
|
||||||
const { pkg, error } = loadApp(dir)
|
const { pkg, error } = loadApp(dir)
|
||||||
const state: AppState = error ? 'invalid' : 'stopped'
|
const state: AppState = error ? 'invalid' : 'stopped'
|
||||||
const icon = pkg.toes?.icon
|
const icon = pkg.toes?.icon ?? DEFAULT_EMOJI
|
||||||
_apps.set(dir, { name: dir, state, icon, error })
|
_apps.set(dir, { name: dir, state, icon, error })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Start all valid apps */
|
export const runApps = () =>
|
||||||
export const runApps = () => {
|
allAppDirs().filter(isApp).forEach(startApp)
|
||||||
for (const dir of appNames()) {
|
|
||||||
const port = getPort()
|
|
||||||
runApp(dir, port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoadResult = { pkg: any; error?: string }
|
type LoadResult = { pkg: any; error?: string }
|
||||||
|
|
||||||
|
|
@ -197,6 +189,7 @@ export const getApp = (dir: string): App | undefined => _apps.get(dir)
|
||||||
export const startApp = (dir: string) => {
|
export const startApp = (dir: string) => {
|
||||||
const app = _apps.get(dir)
|
const app = _apps.get(dir)
|
||||||
if (!app || app.state !== 'stopped') return
|
if (!app || app.state !== 'stopped') return
|
||||||
|
if (!isApp(dir)) return
|
||||||
runApp(dir, getPort())
|
runApp(dir, getPort())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,16 @@ app.get('/api/apps/stream', c => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.get('/api/apps', c => {
|
||||||
|
const apps = allApps().map(app => {
|
||||||
|
const clone = { ...app }
|
||||||
|
delete clone.proc
|
||||||
|
delete clone.logs
|
||||||
|
return clone
|
||||||
|
})
|
||||||
|
return c.json(apps)
|
||||||
|
})
|
||||||
|
|
||||||
app.post('/api/apps/:app/start', c => {
|
app.post('/api/apps/:app/start', c => {
|
||||||
const appName = c.req.param('app')
|
const appName = c.req.param('app')
|
||||||
if (!appName) return c.json({ error: 'App not found' }, 404)
|
if (!appName) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export type LogLine = {
|
||||||
export type App = {
|
export type App = {
|
||||||
name: string
|
name: string
|
||||||
state: AppState
|
state: AppState
|
||||||
icon?: string
|
icon: string
|
||||||
error?: string
|
error?: string
|
||||||
port?: number
|
port?: number
|
||||||
started?: number
|
started?: number
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user