refactor: Update component specifications to use 'program' and 'behavior' attributes, enhancing clarity and consistency across the application
This commit is contained in:
@@ -5,7 +5,7 @@ const TEMPLATES: Record<string, Record<string, unknown>> = {
|
||||
service: {
|
||||
run: {
|
||||
runner: "python",
|
||||
tool: "",
|
||||
program: "",
|
||||
cwd: "",
|
||||
env: {},
|
||||
},
|
||||
@@ -23,9 +23,7 @@ const TEMPLATES: Record<string, Record<string, unknown>> = {
|
||||
},
|
||||
},
|
||||
tool: {
|
||||
install: {
|
||||
path: { alias: "" },
|
||||
},
|
||||
behavior: "tool",
|
||||
},
|
||||
job: {
|
||||
run: {
|
||||
@@ -72,7 +70,7 @@ export function AddComponent({ onAdd, existingNames }: AddComponentProps) {
|
||||
// Fill in template-specific fields
|
||||
if (template === "service") {
|
||||
const run = config.run as Record<string, unknown>
|
||||
run.tool = name
|
||||
run.program = name
|
||||
run.cwd = name
|
||||
const proxy = (config.proxy as Record<string, Record<string, string>>).caddy
|
||||
proxy.path_prefix = `/${name}`
|
||||
@@ -81,8 +79,7 @@ export function AddComponent({ onAdd, existingNames }: AddComponentProps) {
|
||||
expose.http.internal.port = parseInt(port, 10)
|
||||
}
|
||||
} else if (template === "tool") {
|
||||
const install = (config.install as Record<string, Record<string, string>>).path
|
||||
install.alias = name
|
||||
// tool template has behavior preset, no extra config needed
|
||||
}
|
||||
|
||||
await onAdd(name, config)
|
||||
|
||||
@@ -115,7 +115,7 @@ export function ComponentFields({ component, onSave, onDelete }: ComponentFields
|
||||
<Field label="Runner">
|
||||
<span className="text-sm font-mono text-[var(--muted)]">
|
||||
{runnerLabel(runner)}
|
||||
{(m.run as Record<string, string>)?.tool && (
|
||||
{(m.run as Record<string, string>)?.program && (
|
||||
<> · {(m.run as Record<string, string>).tool}</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -32,11 +32,18 @@ const ACTION_CONFIG: Record<string, ActionConfig> = {
|
||||
|
||||
const DEV_ACTIONS = ["build", "test", "lint", "type-check", "check"]
|
||||
|
||||
export interface ActionOutput {
|
||||
action: string
|
||||
text: string
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
interface ProgramActionsProps {
|
||||
name: string
|
||||
actions: string[]
|
||||
installed?: boolean | null
|
||||
compact?: boolean
|
||||
onOutput?: (output: ActionOutput) => void
|
||||
}
|
||||
|
||||
function visibleActions(
|
||||
@@ -69,30 +76,27 @@ function visibleActions(
|
||||
return visible
|
||||
}
|
||||
|
||||
export function ProgramActions({ name, actions, installed, compact }: ProgramActionsProps) {
|
||||
export function ProgramActions({ name, actions, installed, compact, onOutput }: ProgramActionsProps) {
|
||||
const { mutate, isPending } = useProgramAction()
|
||||
const [runningAction, setRunningAction] = useState<string | null>(null)
|
||||
const [output, setOutput] = useState<{ action: string; text: string; ok: boolean } | null>(null)
|
||||
|
||||
const visible = visibleActions(actions, installed, !!compact)
|
||||
|
||||
const handleAction = (action: string) => {
|
||||
setRunningAction(action)
|
||||
setOutput(null)
|
||||
onOutput?.({ action: "", text: "", ok: true }) // clear previous
|
||||
mutate(
|
||||
{ name, action },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setRunningAction(null)
|
||||
// Show output for dev actions on detail page
|
||||
if (!compact && DEV_ACTIONS.includes(action) && data.output) {
|
||||
setOutput({ action, text: data.output, ok: true })
|
||||
onOutput?.({ action, text: data.output, ok: true })
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
setRunningAction(null)
|
||||
if (!compact && DEV_ACTIONS.includes(action)) {
|
||||
// Extract detail from API error JSON
|
||||
let text = String(err)
|
||||
try {
|
||||
const parsed = JSON.parse((err as Error).message)
|
||||
@@ -100,7 +104,7 @@ export function ProgramActions({ name, actions, installed, compact }: ProgramAct
|
||||
} catch {
|
||||
text = (err as Error).message ?? text
|
||||
}
|
||||
setOutput({ action, text, ok: false })
|
||||
onOutput?.({ action, text, ok: false })
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -110,58 +114,59 @@ export function ProgramActions({ name, actions, installed, compact }: ProgramAct
|
||||
if (visible.length === 0) return null
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{visible.map((action) => {
|
||||
const config = ACTION_CONFIG[action]
|
||||
if (!config) return null
|
||||
const Icon = config.icon
|
||||
const isRunning = isPending && runningAction === action
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => handleAction(action)}
|
||||
disabled={isPending}
|
||||
className={`p-1 rounded ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
title={config.label}
|
||||
>
|
||||
{isRunning ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{visible.map((action) => {
|
||||
const config = ACTION_CONFIG[action]
|
||||
if (!config) return null
|
||||
const Icon = config.icon
|
||||
const isRunning = isPending && runningAction === action
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => handleAction(action)}
|
||||
disabled={isPending}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 text-sm rounded border ${config.borderColor} ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
className={`p-1 rounded ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
title={config.label}
|
||||
>
|
||||
{isRunning ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
|
||||
{config.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
{output && !compact && (
|
||||
<div className={`mt-3 rounded-lg border overflow-hidden ${output.ok ? "border-[var(--border)]" : "border-red-800"}`}>
|
||||
<div className={`flex items-center justify-between px-3 py-1.5 text-xs ${output.ok ? "text-green-400 bg-green-900/20" : "text-red-400 bg-red-900/20"}`}>
|
||||
<span>{ACTION_CONFIG[output.action]?.label ?? output.action} — {output.ok ? "ok" : "error"}</span>
|
||||
<button
|
||||
onClick={() => setOutput(null)}
|
||||
className="text-[var(--muted)] hover:text-[var(--foreground)]"
|
||||
>
|
||||
dismiss
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 text-xs font-mono text-gray-300 overflow-x-auto max-h-64 overflow-y-auto bg-black/40">
|
||||
{output.text}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => handleAction(action)}
|
||||
disabled={isPending}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 text-sm rounded border ${config.borderColor} ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
>
|
||||
{isRunning ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
|
||||
{config.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ActionOutputPanel({ output, onDismiss }: { output: ActionOutput; onDismiss: () => void }) {
|
||||
if (!output.action) return null
|
||||
return (
|
||||
<div className={`rounded-lg border overflow-hidden ${output.ok ? "border-[var(--border)]" : "border-red-800"}`}>
|
||||
<div className={`flex items-center justify-between px-3 py-1.5 text-xs ${output.ok ? "text-green-400 bg-green-900/20" : "text-red-400 bg-red-900/20"}`}>
|
||||
<span>{ACTION_CONFIG[output.action]?.label ?? output.action} — {output.ok ? "ok" : "error"}</span>
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="text-[var(--muted)] hover:text-[var(--foreground)]"
|
||||
>
|
||||
dismiss
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 text-xs font-mono text-gray-300 overflow-x-auto max-h-64 overflow-y-auto bg-black/40">
|
||||
{output.text}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState } from "react"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useProgram, useEventStream, useToolDetail } from "@/services/api/hooks"
|
||||
import { runnerLabel } from "@/lib/labels"
|
||||
import { DetailHeader } from "@/components/detail/DetailHeader"
|
||||
import { ConfigPanel } from "@/components/detail/ConfigPanel"
|
||||
import { ProgramActions } from "@/components/ProgramActions"
|
||||
import { ProgramActions, ActionOutputPanel, type ActionOutput } from "@/components/ProgramActions"
|
||||
|
||||
export function ComponentDetailPage() {
|
||||
useEventStream()
|
||||
@@ -11,6 +12,7 @@ export function ComponentDetailPage() {
|
||||
const { data: component, isLoading, error, refetch } = useProgram(name ?? "")
|
||||
const isTool = component?.behavior === "tool"
|
||||
const { data: toolDetail } = useToolDetail(isTool ? (name ?? "") : "")
|
||||
const [actionOutput, setActionOutput] = useState<ActionOutput | null>(null)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -37,9 +39,15 @@ export function ComponentDetailPage() {
|
||||
stack={component.stack}
|
||||
source={component.source}
|
||||
>
|
||||
<ProgramActions name={component.id} actions={component.actions} installed={component.installed} />
|
||||
<ProgramActions name={component.id} actions={component.actions} installed={component.installed} onOutput={setActionOutput} />
|
||||
</DetailHeader>
|
||||
|
||||
{actionOutput && actionOutput.action && (
|
||||
<div className="-mt-2 mb-6">
|
||||
<ActionOutputPanel output={actionOutput} onDismiss={() => setActionOutput(null)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{component.description && (
|
||||
<p className="text-sm text-[var(--muted)] -mt-4 mb-6">{component.description}</p>
|
||||
)}
|
||||
|
||||
@@ -89,8 +89,8 @@ export function ServiceDetailPage() {
|
||||
<span className="flex items-center gap-1">
|
||||
<Terminal size={12} />
|
||||
{runnerLabel(runner)}
|
||||
{(component.manifest.run as Record<string, string>)?.tool && (
|
||||
<> · {(component.manifest.run as Record<string, string>).tool}</>
|
||||
{(component.manifest.run as Record<string, string>)?.program && (
|
||||
<> · {(component.manifest.run as Record<string, string>).program}</>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user